Paramètres optionnels et tableaux de paramètres pour les lambdas et les groupes de méthodes
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Elle inclut les changements de spécification proposés, ainsi que les informations nécessaires à la conception et au développement de la fonctionnalité. Ces articles sont publiés jusqu'à ce que les changements proposés soient finalisés et incorporés dans la spécification ECMA actuelle.
Il peut y avoir des différences entre la spécification de la fonctionnalité et l'implémentation réalisée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Pour en savoir plus sur le processus d'adoption des speclets de fonctionnalité dans la norme du langage C#, consultez l'article sur les spécifications.
Problème de champion : https://github.com/dotnet/csharplang/issues/6051
Récapitulatif
Pour compléter les améliorations apportées aux lambdas en C# 10 (voir le contexte pertinent), nous proposons d'ajouter la prise en charge des valeurs de paramètres par défaut et des tableaux params
dans les lambdas. Cela permettrait aux utilisateurs d’implémenter les lambdas suivantes :
var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3
De même, nous autoriserons le même type de comportement pour les groupes de méthodes :
var addWithDefault = AddWithDefaultMethod;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = CountMethod;
counter(); // 0
counter(1, 2); // 2
int AddWithDefaultMethod(int addTo = 2) {
return addTo + 1;
}
int CountMethod(params int[] xs) {
return xs.Length;
}
Arrière-plan pertinent
Améliorations lambda dans C# 10
Spécification de conversion de groupe de méthodes §10.8
Motivation
Les infrastructures d’application dans l’écosystème .NET tirent largement parti des lambdas pour permettre aux utilisateurs d’écrire rapidement une logique métier associée à un point de terminaison.
var app = WebApplication.Create(args);
app.MapPost("/todos/{id}", (TodoService todoService, int id, string task) => {
var todo = todoService.Create(id, task);
return Results.Created(todo);
});
Actuellement, les lambdas ne prennent pas en charge la définition des valeurs par défaut sur les paramètres. Par conséquent, si un développeur souhaitait créer une application résiliente aux scénarios où les utilisateurs n’ont pas fourni de données, il devrait soit utiliser des fonctions locales, soit définir les valeurs par défaut dans le corps lambda, par opposition à la syntaxe proposée plus succincte.
var app = WebApplication.Create(args);
app.MapPost("/todos/{id}", (TodoService todoService, int id, string task = "foo") => {
var todo = todoService.Create(id, task);
return Results.Created(todo);
});
La syntaxe proposée présente également l’avantage de réduire les différences déroutantes entre les fonctions lambdas et locales, ce qui facilite la compréhension des constructions et permet de faire évoluer les lambdas en fonctions sans compromettre les fonctionnalités, en particulier dans d’autres scénarios où les lambdas sont utilisées dans les API où les groupes de méthodes peuvent également être passés en tant que références.
C’est aussi la principale motivation pour prendre en charge le tableau params
qui n’est pas couvert par le scénario de cas d’utilisation mentionné ci-dessus.
Exemple :
var app = WebApplication.Create(args);
Result TodoHandler(TodoService todoService, int id, string task = "foo") {
var todo = todoService.Create(id, task);
return Results.Created(todo);
}
app.MapPost("/todos/{id}", TodoHandler);
Comportement précédent
Avant C# 12, lorsqu’un utilisateur implémente une lambda avec un paramètre facultatif ou params
, le compilateur génère une erreur.
var addWithDefault = (int addTo = 2) => addTo + 1; // error CS1065: Default values are not valid in this context.
var counter = (params int[] xs) => xs.Length; // error CS1670: params is not valid in this context
Lorsqu’un utilisateur tente d’utiliser un groupe de méthodes où la méthode sous-jacente a un paramètre facultatif ou params
, ces informations ne sont pas propagées, de sorte que l’appel à la méthode ne passe pas la vérification de type en raison d’un décalage dans le nombre d’arguments attendus.
void M1(int i = 1) { }
var m1 = M1; // Infers Action<int>
m1(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int>'
void M2(params int[] xs) { }
var m2 = M2; // Infers Action<int[]>
m2(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int[]>'
Nouveau comportement
Après cette proposition (faisant partie de C# 12), les valeurs par défaut et params
peuvent être appliqués aux paramètres lambda avec le comportement suivant :
var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3
Les valeurs par défaut et params
peuvent être appliqués aux paramètres de groupe de méthodes en définissant spécifiquement ce groupe de méthodes :
int AddWithDefault(int addTo = 2) {
return addTo + 1;
}
var add1 = AddWithDefault;
add1(); // ok, default parameter value will be used
int Counter(params int[] xs) {
return xs.Length;
}
var counter1 = Counter;
counter1(1, 2, 3); // ok, `params` will be used
Modification avec rupture
Avant C# 12, le type déduit d’un groupe de méthodes est Action
ou Func
, donc le code suivant se compile :
void WriteInt(int i = 0) {
Console.Write(i);
}
var writeInt = WriteInt; // Inferred as Action<int>
DoAction(writeInt, 3); // Ok, writeInt is an Action<int>
void DoAction(Action<int> a, int p) {
a(p);
}
int Count(params int[] xs) {
return xs.Length;
}
var counter = Count; // Inferred as Func<int[], int>
DoFunction(counter, 3); // Ok, counter is a Func<int[], int>
int DoFunction(Func<int[], int> f, int p) {
return f(new[] { p });
}
Après cette modification (faisant partie de C# 12), le code de cette nature cesse de se compiler dans le Kit de développement logiciel (SDK) .NET 7.0.200 ou version ultérieure.
void WriteInt(int i = 0) {
Console.Write(i);
}
var writeInt = WriteInt; // Inferred as anonymous delegate type
DoAction(writeInt, 3); // Error, cannot convert from anonymous delegate type to Action
void DoAction(Action<int> a, int p) {
a(p);
}
int Count(params int[] xs) {
return xs.Length;
}
var counter = Count; // Inferred as anonymous delegate type
DoFunction(counter, 3); // Error, cannot convert from anonymous delegate type to Func
int DoFunction(Func<int[], int> f, int p) {
return f(new[] { p });
}
L’impact de cette modification majeure doit être pris en compte. Heureusement, l’utilisation de var
pour déduire le type d’un groupe de méthodes n’est prise en charge que depuis C# 10, donc seul le code écrit depuis lors et qui repose explicitement sur ce comportement serait affecté.
Conception détaillée
Modifications de grammaire et d’analyseur
Cette amélioration nécessite les modifications suivantes de la grammaire pour les expressions lambda.
lambda_expression
: modifier* identifier '=>' (block | expression)
- | attribute_list* modifier* type? lambda_parameters '=>' (block | expression)
+ | attribute_list* modifier* type? lambda_parameter_list '=>' (block | expression)
;
+lambda_parameter_list
+ : lambda_parameters (',' parameter_array)?
+ | parameter_array
+ ;
lambda_parameter
: identifier
- | attribute_list* modifier* type? identifier
+ | attribute_list* modifier* type? identifier default_argument?
;
Notez que cela autorise les valeurs par défaut des paramètres et les tableaux params
uniquement pour les lambdas, et non pour les méthodes anonymes déclarées avec la syntaxe delegate { }
.
Les mêmes règles que pour les paramètres de méthode (§15.6.2) s’appliquent aux paramètres lambda :
- Un paramètre avec un modificateur
ref
,out
outhis
ne peut pas avoir un argument_par_défaut . - Un parameter_array peut apparaître après un paramètre facultatif, mais il ne peut pas avoir de valeur par défaut (l’omission d’arguments pour un parameter_array entraînerait plutôt la création d’un tableau vide).
Aucune modification de la grammaire n’est nécessaire pour les groupes de méthodes, car cette proposition ne modifierait que leur sémantique.
L’ajout suivant (en gras) est requis pour les conversions de fonctions anonymes (§10.7) :
Plus précisément, une fonction anonyme
F
est compatible avec un type de déléguéD
à condition que :
- [...]
- Si
F
a une liste de paramètres explicitement typée, chaque paramètre deD
a le même type et les mêmes modificateurs que le paramètre correspondant dansF
, en ignorant les modificateursparams
et les valeurs par défaut.
Mises à jour des propositions antérieures
L'ajout suivant (en gras) est requis pour la spécification des types de fonctions dans une proposition antérieure :
Un groupe de méthodes a un type naturel si toutes les méthodes candidates du groupe de méthodes ont une signature commune, y compris les valeurs par défaut et les modificateurs
params
. (Si le groupe de méthodes peut inclure des méthodes d'extension, les candidats incluent le type contenant et toutes les portées des méthodes d'extension).
Le type naturel d’une expression de fonction anonyme ou d’un groupe de méthodes est un function_type. Un function_type représente la signature d'une méthode : les types de paramètres, les valeurs par défaut, les types de ref
params
, les modificateurs, le type de retour et le type de ref. Les expressions de fonctions anonymes ou les groupes de méthodes avec la même signature ont le même function_type.
L’ajout suivant (en gras) est requis à la spécification des types de délégués dans une proposition antérieure :
Le type de délégué pour la fonction anonyme ou le groupe de méthodes avec des types de paramètres
P1, ..., Pn
et le type de retourR
est :
- si un paramètre ou une valeur de retour n’est pas par valeur, , ou si n’importe quel paramètre est facultatif, ou
params
, ou s'il y a plus de 16 paramètres, ou si l’un des types de paramètres ou le retour n’est pas un argument de type valide (par exemple,(int* p) => { }
), le délégué est un type de délégué anonyme synthétiséinternal
avec une signature qui correspond à la fonction anonyme ou au groupe de méthodes, et avec des noms de paramètresarg1, ..., argn
ouarg
si un paramètre unique.
Changements dans les classeurs
Synthèse des nouveaux types de délégués
Comme pour le comportement des délégués avec des paramètres ref
ou out
, les types de délégués sont synthétisés pour les lambdas ou les groupes de méthodes définis avec des paramètres facultatifs ou params
.
Notez que, dans les exemples ci-dessous, la notation a'
, b'
, etc. est utilisée pour représenter ces types de délégués anonymes.
var addWithDefault = (int addTo = 2) => addTo + 1;
// internal delegate int a'(int arg = 2);
var printString = (string toPrint = "defaultString") => Console.WriteLine(toPrint);
// internal delegate void b'(string arg = "defaultString");
var counter = (params int[] xs) => xs.Length;
// internal delegate int c'(params int[] arg);
string PathJoin(string s1, string s2, string sep = "/") { return $"{s1}{sep}{s2}"; }
var joinFunc = PathJoin;
// internal delegate string d'(string arg1, string arg2, string arg3 = " ");
Comportement de conversion et d’unification
Les délégués anonymes avec des paramètres facultatifs seront unifiés lorsque le même paramètre (basé sur la position) a la même valeur par défaut, quel que soit le nom du paramètre.
int E(int j = 13) {
return 11;
}
int F(int k = 0) {
return 3;
}
int G(int x = 13) {
return 4;
}
var a = (int i = 13) => 1;
// internal delegate int b'(int arg = 13);
var b = (int i = 0) => 2;
// internal delegate int c'(int arg = 0);
var c = (int i = 13) => 3;
// internal delegate int b'(int arg = 13);
var d = (int c = 13) => 1;
// internal delegate int b'(int arg = 13);
var e = E;
// internal delegate int b'(int arg = 13);
var f = F;
// internal delegate int c'(int arg = 0);
var g = G;
// internal delegate int b'(int arg = 13);
a = b; // Not allowed
a = c; // Allowed
a = d; // Allowed
c = e; // Allowed
e = f; // Not Allowed
b = f; // Allowed
e = g; // Allowed
d = (int c = 10) => 2; // Warning: default parameter value is different between new lambda
// and synthesized delegate b'. We won't do implicit conversion
Les délégués anonymes avec un tableau comme dernier paramètre seront unifiés lorsque le dernier paramètre a le même modificateur params
et le même type de tableau, quel que soit le nom du paramètre.
int C(int[] xs) {
return xs.Length;
}
int D(params int[] xs) {
return xs.Length;
}
var a = (int[] xs) => xs.Length;
// internal delegate int a'(int[] xs);
var b = (params int[] xs) => xs.Length;
// internal delegate int b'(params int[] xs);
var c = C;
// internal delegate int a'(int[] xs);
var d = D;
// internal delegate int b'(params int[] xs);
a = b; // Not allowed
a = c; // Allowed
b = c; // Not allowed
b = d; // Allowed
c = (params int[] xs) => xs.Length; // Warning: different delegate types; no implicit conversion
d = (int[] xs) => xs.Length; // OK. `d` is `delegate int (params int[] arg)`
De même, il y a bien sûr compatibilité avec les délégués nommés qui supportent déjà les paramètres optionnels et params
.
Lorsque les valeurs par défaut ou les modificateurs params
diffèrent dans une conversion, ceux de la source ne seront pas utilisés s’ils se trouvent dans une expression lambda, car l’expression lambda ne peut pas être appelée d’une autre manière.
Cela peut sembler contre-intuitif pour les utilisateurs. Par conséquent, un avertissement sera émis lorsque la valeur par défaut de la source ou le modificateur params
est présent et différent de celui de la cible.
Si la source est un groupe de méthodes, elle peut être appelée seule. Par conséquent, aucun avertissement ne sera émis.
delegate int DelegateNoDefault(int x);
delegate int DelegateWithDefault(int x = 1);
int MethodNoDefault(int x) => x;
int MethodWithDefault(int x = 2) => x;
DelegateNoDefault d1 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d2 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d3 = MethodNoDefault; // no warning: source is a method group
DelegateNoDefault d4 = (int x = 1) => x; // warning: source present, target missing
DelegateWithDefault d5 = (int x = 2) => x; // warning: source present, target different
DelegateWithDefault d6 = (int x) => x; // no warning: source missing, target present
delegate int DelegateNoParams(int[] xs);
delegate int DelegateWithParams(params int[] xs);
int MethodNoParams(int[] xs) => xs.Length;
int MethodWithParams(params int[] xs) => xs.Length;
DelegateNoParams d7 = MethodWithParams; // no warning: source is a method group
DelegateWithParams d8 = MethodNoParams; // no warning: source is a method group
DelegateNoParams d9 = (params int[] xs) => xs.Length; // warning: source present, target missing
DelegateWithParams d10 = (int[] xs) => xs.Length; // no warning: source missing, target present
Comportement IL/runtime
Les valeurs par défaut des paramètres seront émises dans les métadonnées. L'IL pour cette fonctionnalité sera de nature très similaire à l'IL émise pour les lambdas avec des paramètres ref
et out
. Une classe qui hérite de System.Delegate
ou similaire sera générée, et la méthode Invoke
inclura des directives .param
pour définir des valeurs par défaut des paramètres ou System.ParamArrayAttribute
(tout comme c’est le cas pour un délégué nommé standard avec des paramètres facultatifs ou params
).
Ces types de délégués peuvent être inspectés à l'exécution, comme d'habitude.
Dans le code, les utilisateurs peuvent introspecter le DefaultValue
dans le ParameterInfo
associé à la lambda ou au groupe de méthodes en utilisant le MethodInfo
associé.
var addWithDefault = (int addTo = 2) => addTo + 1;
int AddWithDefaultMethod(int addTo = 2)
{
return addTo + 1;
}
var defaultParm = addWithDefault.Method.GetParameters()[0].DefaultValue; // 2
var add1 = AddWithDefaultMethod;
defaultParm = add1.Method.GetParameters()[0].DefaultValue; // 2
Questions ouvertes
Aucune de ces propositions n’a été implémentée. Elle restent des propositions ouvertes.
Question ouverte : comment cela interagit-il avec l’attribut DefaultParameterValue
existant ?
Réponse proposée : Pour la parité, autorisez l’attribut DefaultParameterValue
sur les lambdas et assurez-vous que le comportement de génération de délégué correspond aux valeurs par défaut des paramètres prises en charge par la syntaxe.
var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed
Question ouverte : Premièrement, notez que cela dépasse le cadre de la proposition actuelle, mais il pourrait être intéressant de discuter à l'avenir. Voulons-nous soutenir des valeurs par défaut avec des paramètres lambda implicitement typés ? Autrement dit :
delegate void M1(int i = 3);
M1 m = (x = 3) => x + x; // Ok
delegate void M2(long i = 2);
M2 m = (x = 3.0) => ...; //Error: cannot convert implicitly from long to double
Cette inférence entraîne des problèmes de conversion délicats qui nécessiteraient une discussion plus approfondie.
Il y a également des considérations de performance d'analyse ici. Par exemple, aujourd’hui, le terme (x =
ne pourrait jamais être le début d’une expression lambda. **
Si cette syntaxe était autorisée pour les valeurs par défaut des lambda, l'analyseur aurait besoin d'une anticipation plus importante en analysant jusqu'à rencontrer un jeton =>
afin de déterminer s'il s'agit ou non d'un terme lambda.
Réunions de conception
- LDM 2022-10-10 : décision d'ajouter la prise en charge de
params
de la même manière que les valeurs de paramètres par défaut.
C# feature specifications