Partage via


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 :

  1. Autoriser les lambda avec des attributs
  2. Autoriser les lambda avec un type de retour explicite
  3. 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_typeG si les paramètres et les types de retour de F sont convertibles en variance vers les paramètres et le type de retour de G
  • En System.MulticastDelegate ou des classes de base ou des interfaces de System.MulticastDelegate
  • En System.Linq.Expressions.Expression ou System.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 et Ti 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 de Ei en Tiet une inférence de type de retour explicite est effectuée de Ei en Ti .
  • Sinon, si Ei a un type U et xi est un paramètre de valeur, une inférence à limite inférieure est effectuée deUàTi.
  • Sinon, si Ei a un type U et xi est un paramètre ref ou out, 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 Evers un type T de la manière suivante :

  • Si E est une fonction anonyme avec un type de retour explicite Ur et T est un type délégué ou un type d’arborescence d’expressions avec le type de retour Vr, 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 candidatsUj commence par l’ensemble de tous les types dans l’ensemble de bornes pour Xioù 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 exact U de Xi, tous les types Uj qui ne sont pas identiques à U sont supprimés de l’ensemble de candidats. Pour chaque borne inférieure U de Xi, tous les types Uj vers lesquels il n'existe pas de conversion implicite à partir de U sont supprimés de l'ensemble des candidats. Pour chaque borne supérieure U de Xi, tous les types Uj à partir desquels il n’y a pas de conversion implicite vers U sont retirés de l’ensemble des candidats.
  • Si parmi les types candidats restants Uj il existe un type unique V à 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é anonyme internal synthétisé avec une signature qui correspond à la fonction anonyme ou au groupe de méthodes, et avec des noms de paramètres arg1, ..., argn ou arg si un paramètre unique ;
  • si R est void, le type délégué est System.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 applicables Mp et Mq avec des types de paramètres {P1, P2, ..., Pn} et {Q1, Q2, ..., Qn}, Mp est défini pour être un membre de fonction meilleur que Mq si

  1. pour chaque argument, la conversion implicite de Ex en Px n’est pas une function_type_conversion, et
    • Mp est une méthode non générique ou Mp est une méthode générique avec des paramètres de type {X1, X2, ..., Xp} et pour chaque paramètre de type Xi 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 en Qx est un function_type_conversion, ou Mq est une méthode générique avec des paramètres de type {Y1, Y2, ..., Yq} et pour au moins un paramètre de type Yi l’argument de type est déduit d’un function_type, ou
  2. pour chaque argument, la conversion implicite de Ex en Qx n’est pas meilleure que la conversion implicite de Ex en Px, et pour au moins un argument, la conversion de Ex en Px est meilleure que la conversion de Ex en Qx.

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 expression E vers un type T1, et une conversion implicite C2 qui convertit d'une expression E vers un type T2, C1 est une meilleure conversion que C2 si :

  1. C1 n’est pas un function_type_conversion et C2 est un function_type_conversion, ou
  2. E 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 , et C2 n’est pas une conversion implicite de gestionnaire de chaîne , ou
  3. E ne correspond pas exactement à T2 et au moins l’une des conditions suivantes est remplie :
    • E correspond exactement à T1 (§12.6.4.5)
    • T1 est une meilleure cible de conversion que T2 (§12.6.4.7)

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 ?