Améliorations lambda
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Il inclut les modifications de spécification proposées, ainsi que les informations nécessaires pendant la conception et le développement de la fonctionnalité. Ces articles sont publiés jusqu’à ce que les modifications de spécification proposées soient finalisées et incorporées dans la spécification ECMA actuelle.
Il peut y avoir des différences entre la spécification de la fonctionnalité et l’implémentation terminée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Vous pouvez en savoir plus sur le processus d’adoption des speclets de fonctionnalités dans la norme de langage C# dans l’article sur les spécifications .
Problème de champion : https://github.com/dotnet/csharplang/issues/4934
Résumé
Modifications proposées :
- Autoriser les lambda avec des attributs
- Autoriser les lambda avec un type de retour explicite
- Déduire un type délégué naturel pour les groupes lambda et de méthodes
Motivation
La prise en charge des attributs sur les expressions lambdas fournirait une parité avec les méthodes et les fonctions locales.
La prise en charge des types de retour explicites fournit une symétrie avec des paramètres lambda où des types explicites peuvent être spécifiés. Permettre des types de retour explicites offrirait également la possibilité de contrôler les performances du compilateur dans les lambdas imbriquées, où la résolution de surcharge doit actuellement lier le corps de la lambda pour en déterminer la signature.
Un type naturel pour les expressions lambda et les groupes de méthodes permet d’utiliser davantage de scénarios où les groupes lambda et de méthode peuvent être utilisés sans type délégué explicite, y compris en tant qu’initialiseurs dans les déclarations var
.
Exiger des types délégués explicites pour les groupes lambda et de méthodes a été un point de friction pour les clients, et est devenu un obstacle à la progression dans ASP.NET avec des travaux récents sur MapAction.
ASP.NET mapAction sans modifications proposées (MapAction()
prend un argument System.Delegate
) :
[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction((Func<Todo>)GetTodo);
[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction((Func<Todo, Todo>)PostTodo);
ASP.NET MapAction avec des types intrinsèques pour les groupes de méthodes :
[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction(GetTodo);
[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction(PostTodo);
ASP.NET MapAction avec des attributs et des types naturels pour les expressions lambda :
app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo);
Attributs
Les attributs peuvent être ajoutés aux expressions lambda et aux paramètres lambda. Pour éviter toute ambiguïté entre les attributs de méthode et les attributs de paramètre, une expression lambda avec des attributs doit utiliser une liste de paramètres entre parenthèses. Les types de paramètres ne sont pas obligatoires.
f = [A] () => { }; // [A] lambda
f = [return:A] x => x; // syntax error at '=>'
f = [return:A] (x) => x; // [A] lambda
f = [A] static x => x; // syntax error at '=>'
f = ([A] x) => x; // [A] x
f = ([A] ref int x) => x; // [A] x
Plusieurs attributs peuvent être spécifiés, séparés par des virgules dans la même liste d’attributs ou en tant que listes d’attributs distinctes.
var f = [A1, A2][A3] () => { }; // ok
var g = ([A1][A2, A3] int x) => x; // ok
Les attributs ne sont pas pris en charge pour les méthodes anonymes déclarées avec une syntaxe delegate { }
.
f = [A] delegate { return 1; }; // syntax error at 'delegate'
f = delegate ([A] int x) { return x; }; // syntax error at '['
L'analyseur analysera la différenciation entre un initialiseur de collection avec une affectation d'élément et un initialiseur de collection avec une expression lambda.
var y = new C { [A] = x }; // ok: y[A] = x
var z = new C { [A] x => x }; // ok: z[0] = [A] x => x
L’analyseur traite ?[
comme début d’un accès d’élément conditionnel.
x = b ? [A]; // ok
y = b ? [A] () => { } : z; // syntax error at '('
Les attributs de l’expression lambda ou des paramètres lambda sont émis dans les métadonnées de la méthode qui est mappée à l’expression lambda.
En général, les clients ne doivent pas dépendre de la façon dont les expressions lambda et les fonctions locales sont mappées de la source aux métadonnées. La façon dont les fonctions lambda et locales sont émises peut et a changé entre les versions du compilateur.
Les modifications proposées ici sont ciblées sur le scénario piloté par Delegate
.
Il doit être valide pour inspecter la MethodInfo
associée à une instance de Delegate
pour déterminer la signature de l’expression lambda ou de la fonction locale, y compris les attributs explicites et les métadonnées supplémentaires émises par le compilateur, telles que les paramètres par défaut.
Cela permet aux équipes telles que ASP.NET de rendre disponibles les mêmes comportements pour les fonctions lambda et locales que les méthodes ordinaires.
Type de retour explicite
Un type de retour explicite peut être spécifié avant la liste des paramètres entre parenthèses.
f = T () => default; // ok
f = short x => 1; // syntax error at '=>'
f = ref int (ref int x) => ref x; // ok
f = static void (_) => { }; // ok
f = async async (async async) => async; // ok?
L’analyseur va envisager de différencier un appel de méthode T()
d’une expression lambda T () => e
.
Les types de retour explicites ne sont pas pris en charge pour les méthodes anonymes déclarées avec delegate { }
syntaxe.
f = delegate int { return 1; }; // syntax error
f = delegate int (int x) { return x; }; // syntax error
L’inférence de type de méthode doit effectuer une inférence exacte à partir d’un type de retour lambda explicite.
static void F<T>(Func<T, T> f) { ... }
F(int (i) => i); // Func<int, int>
Les conversions de variance ne sont pas autorisées du type de retour lambda au type de retour délégué (comportement similaire pour les types de paramètres).
Func<object> f1 = string () => null; // error
Func<object?> f2 = object () => x; // warning
L’analyseur autorise les expressions lambda avec des types de retour ref
au sein des expressions sans parenthèses supplémentaires.
d = ref int () => x; // d = (ref int () => x)
F(ref int () => x); // F((ref int () => x))
var
ne peut pas être utilisé comme type de retour explicite pour les expressions lambda.
class var { }
d = var (var v) => v; // error: contextual keyword 'var' cannot be used as explicit lambda return type
d = @var (var v) => v; // ok
d = ref var (ref var v) => ref v; // error: contextual keyword 'var' cannot be used as explicit lambda return type
d = ref @var (ref var v) => ref v; // ok
Type naturel (fonction)
Une fonction anonyme expression (§12.19) (une expression lambda ou une méthode anonyme ) a un type naturel si les types de paramètres sont explicites et que le type de retour est explicite ou peut être déduit (voir §12.6.3.13).
Un groupe de méthodes a un type naturel si toutes les méthodes candidates du groupe de méthodes ont une signature commune. (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 une signature de méthode : les types de paramètres et les types ref, ainsi que le type de retour et le type ref. Les expressions de fonction anonyme ou les groupes de méthodes avec la même signature ont la même function_type.
Function_types sont utilisés dans quelques contextes spécifiques uniquement :
- conversions implicites et explicites
- inférence de type de méthode (§12.6.3) et type le plus courant (§12.6.3.15)
- Initialiseurs
var
Une function_type existe uniquement au moment de la compilation : function_types n’apparaissent pas dans la source ou les métadonnées.
Conversions
À partir d’un function_typeF
il existe des conversions function_type implicites :
- En un function_type
G
si les paramètres et les types de retour deF
sont convertibles en variance vers les paramètres et le type de retour deG
- En
System.MulticastDelegate
ou des classes de base ou des interfaces deSystem.MulticastDelegate
- En
System.Linq.Expressions.Expression
ouSystem.Linq.Expressions.LambdaExpression
Les expressions anonymes et les groupes de méthodes ont déjà des conversions de l'expression en types délégués et types d’arborescence d’expressions (voir conversions de fonctions anonymes §10.7 et conversions de groupes de méthodes §10.8). Ces conversions sont suffisantes pour la conversion en types délégués fortement typés et en types d'arbres d'expressions. Les conversions function_type ci-dessus ajoutent des conversions à partir d’un type aux types de base uniquement : System.MulticastDelegate
, System.Linq.Expressions.Expression
, etc.
Il n’existe aucune conversion en function_type à partir d’un type autre qu’un function_type. Il n’existe aucune conversion explicite pour function_types, car function_types ne peut pas être référencée dans la source.
Une conversion en System.MulticastDelegate
, type de base, ou interface transforme la fonction anonyme ou le groupe de méthodes en une instance d’un type délégué approprié.
Une conversion en System.Linq.Expressions.Expression<TDelegate>
ou type de base réalise l’expression lambda en tant qu’arborescence d’expressions avec un type délégué approprié.
Delegate d = delegate (object obj) { }; // Action<object>
Expression e = () => ""; // Expression<Func<string>>
object o = "".Clone; // Func<object>
Function_type conversions ne sont pas des conversions standard implicites ou explicites §10.4 et ne sont pas prises en compte lors de la détermination de l’application d’un opérateur de conversion défini par l’utilisateur à une fonction anonyme ou à un groupe de méthodes. À partir de l’évaluation des conversions définies par l’utilisateur §10.5.3:
Pour qu’un opérateur de conversion soit applicable, il doit être possible d’effectuer une conversion standard (§10.4) du type source au type d’opérande de l’opérateur, et il doit être possible d’effectuer une conversion standard du type de résultat de l’opérateur vers le type cible.
class C
{
public static implicit operator C(Delegate d) { ... }
}
C c;
c = () => 1; // error: cannot convert lambda expression to type 'C'
c = (C)(() => 2); // error: cannot convert lambda expression to type 'C'
Un avertissement est signalé pour une conversion implicite d’un groupe de méthodes en object
, car la conversion est valide, mais peut-être involontaire.
Random r = new Random();
object obj;
obj = r.NextDouble; // warning: Converting method group to 'object'. Did you intend to invoke the method?
obj = (object)r.NextDouble; // ok
Inférence de type
Les règles existantes pour l’inférence de type sont principalement inchangées (voir §12.6.3). Toutefois, il existe cependant quelques modifications ci-dessous concernant des phases spécifiques d’inférence de type.
Première phase
La première phase (§12.6.3.2) permet à une fonction anonyme de se lier à Ti
même si Ti
n’est pas un type d’arborescence délégué ou d’expression (peut-être un paramètre de type limité à System.Delegate
par exemple).
Pour chacun des arguments de méthode
Ei
:
- Si
Ei
est une fonction anonyme etTi
est un type délégué ou un type d’arborescence d’expression, une inférence de type de paramètre explicite est effectuée deEi
enTi
et une inférence de type de retour explicite est effectuée deEi
enTi
.- Sinon, si
Ei
a un typeU
etxi
est un paramètre de valeur, une inférence à limite inférieure est effectuée deU
àTi
.- Sinon, si
Ei
a un typeU
etxi
est un paramètreref
ouout
, une inférence exacte est réalisée deU
àTi
.- Sinon, aucune inférence n’est faite pour cet argument.
Inférence de type de retour explicite
Une inférence de type de retour explicite est faite à partir d’une expression
E
vers un typeT
de la manière suivante :
- Si
E
est une fonction anonyme avec un type de retour expliciteUr
etT
est un type délégué ou un type d’arborescence d’expressions avec le type de retourVr
, une d’inférence exacte (§12.6.3.9) est effectuée deUr
àVr
.
Réparation
La correction (§12.6.3.12) garantit que d’autres conversions sont préférées par rapport aux conversions function_type. (Les expressions lambda et les expressions de groupe de méthodes contribuent uniquement aux limites inférieures afin que la gestion des function_types soit nécessaire uniquement pour les limites inférieures.)
Une variable de type
Xi
non fixée avec un ensemble de bornes est fixée comme suit :
- L’ensemble des types candidats
Uj
commence par l’ensemble de tous les types dans l’ensemble de bornes pourXi
où les types de fonction sont ignorés dans les limites inférieures s’il y a des types qui ne sont pas des types de fonction.- Nous examinons ensuite chaque borne de
Xi
: pour chaque borne exactU
deXi
, tous les typesUj
qui ne sont pas identiques àU
sont supprimés de l’ensemble de candidats. Pour chaque borne inférieureU
deXi
, tous les typesUj
vers lesquels il n'existe pas de conversion implicite à partir deU
sont supprimés de l'ensemble des candidats. Pour chaque borne supérieureU
deXi
, tous les typesUj
à partir desquels il n’y a pas de conversion implicite versU
sont retirés de l’ensemble des candidats.- Si parmi les types candidats restants
Uj
il existe un type uniqueV
à partir duquel il existe une conversion implicite vers tous les autres types candidats,Xi
est fixe àV
.- Sinon, l’inférence de type échoue.
Type le plus courant
Le type le plus courant (§12.6.3.15) est défini en termes d’inférence de type, de sorte que les modifications d’inférence de type ci-dessus s’appliquent également au meilleur type commun.
var fs = new[] { (string s) => s.Length, (string s) => int.Parse(s) }; // Func<string, int>[]
var
Les fonctions anonymes et les groupes de méthodes avec des types de fonctions peuvent être utilisés comme initialiseurs dans var
déclarations.
var f1 = () => default; // error: cannot infer type
var f2 = x => x; // error: cannot infer type
var f3 = () => 1; // System.Func<int>
var f4 = string () => null; // System.Func<string>
var f5 = delegate (object o) { }; // System.Action<object>
static void F1() { }
static void F1<T>(this T t) { }
static void F2(this string s) { }
var f6 = F1; // error: multiple methods
var f7 = "".F1; // error: the delegate type could not be inferred
var f8 = F2; // System.Action<string>
Les types de fonctions ne sont pas utilisés dans les attribution à des éléments ignorés.
d = () => 0; // ok
_ = () => 1; // error
Types délégués
Le type 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 retour R
est :
- si un paramètre ou une valeur de retour n’est pas par valeur ou s’il existe plus de 16 paramètres, ou si l’un des types de paramètres ou retour n’est pas des arguments de type valides (par exemple,
(int* p) => { }
), le délégué est un type de délégué anonymeinternal
synthétisé 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 ; - si
R
estvoid
, le type délégué estSystem.Action<P1, ..., Pn>
; - sinon, le type de délégué est
System.Func<P1, ..., Pn, R>
.
Le compilateur peut autoriser davantage de signatures à se lier aux types System.Action<>
et System.Func<>
à l'avenir (si les types ref struct
sont par exemple autorisés comme arguments de type).
modopt()
ou modreq()
dans la signature du groupe de méthodes sont ignorés dans le type délégué correspondant.
Si deux fonctions ou groupes de méthodes anonymes dans la même compilation nécessitent des types délégués synthétisés avec les mêmes types de paramètres et modificateurs et le même type de retour et les mêmes modificateurs, le compilateur utilise le même type délégué synthétisé.
Résolution de surcharge
Un membre de fonction amélioré (§12.6.4.3) est mis à jour pour favoriser les membres pour lesquels aucune des conversions et aucun des arguments de type n’impliquent des types déduits d’expressions lambda ou de groupes de méthodes.
Meilleur membre de fonction
... Étant donné une liste d’arguments
A
avec un ensemble d’expressions d’arguments{E1, E2, ..., En}
et deux membres de fonction applicablesMp
etMq
avec des types de paramètres{P1, P2, ..., Pn}
et{Q1, Q2, ..., Qn}
,Mp
est défini pour être un membre de fonction meilleur queMq
si
- pour chaque argument, la conversion implicite de
Ex
enPx
n’est pas une function_type_conversion, et
Mp
est une méthode non générique ouMp
est une méthode générique avec des paramètres de type{X1, X2, ..., Xp}
et pour chaque paramètre de typeXi
l’argument de type est déduit d’une expression ou d’un type autre qu’un function_typeet- pour au moins un argument, la conversion implicite de
Ex
enQx
est un function_type_conversion, ouMq
est une méthode générique avec des paramètres de type{Y1, Y2, ..., Yq}
et pour au moins un paramètre de typeYi
l’argument de type est déduit d’un function_type, ou- pour chaque argument, la conversion implicite de
Ex
enQx
n’est pas meilleure que la conversion implicite deEx
enPx
, et pour au moins un argument, la conversion deEx
enPx
est meilleure que la conversion deEx
enQx
.
Une meilleure conversion à partir d’une expression (§12.6.4.5) est mise à jour pour préférer les conversions qui n’ont pas impliqué de types déduits à partir d’expressions lambda ou de groupes de méthodes.
Meilleure conversion à partir d’une expression
Étant donné une conversion implicite
C1
qui convertit d'une expressionE
vers un typeT1
, et une conversion impliciteC2
qui convertit d'une expressionE
vers un typeT2
,C1
est une meilleure conversion queC2
si :
C1
n’est pas un function_type_conversion etC2
est un function_type_conversion, ouE
est une expression de chaîne interpolée non constante,C1
est une conversion implicite de gestionnaire de chaîne ,T1
est un type de gestionnaire de chaîne interpolée applicable , etC2
n’est pas une conversion implicite de gestionnaire de chaîne , ouE
ne correspond pas exactement àT2
et au moins l’une des conditions suivantes est remplie :
Syntaxe
lambda_expression
: modifier* identifier '=>' (block | expression)
| attribute_list* modifier* type? lambda_parameters '=>' (block | expression)
;
lambda_parameters
: lambda_parameter
| '(' (lambda_parameter (',' lambda_parameter)*)? ')'
;
lambda_parameter
: identifier
| attribute_list* modifier* type? identifier equals_value_clause?
;
Problèmes ouverts
Les valeurs par défaut doivent-elle être prises en charge pour les paramètres d’expression lambda pour l’exhaustivité ?
Faudrait-il interdire System.Diagnostics.ConditionalAttribute
sur les expressions lambda, puisqu'il y a peu de scénarios où une expression lambda pourrait être utilisée de manière conditionnelle ?
([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?
Le function_type doit-il être disponible à partir de l’API du compilateur, en plus du type délégué résultant ?
Actuellement, le type délégué déduit utilise System.Action<>
ou System.Func<>
lorsque les types de paramètre et de retour sont des arguments de type valides et il n’y a pas plus de 16 paramètres, et si le type Action<>
ou Func<>
attendu est manquant, une erreur est signalée. Au lieu de cela, le compilateur devrait-il utiliser System.Action<>
ou System.Func<>
quelle que soit l’arité ? Et si le type attendu est manquant, synthétisez un type délégué sinon ?
C# feature specifications