Delen via


Lambda-verbeteringen

Notitie

Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.

Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Die verschillen worden vastgelegd in de relevante notities van de Language Design Meeting (LDM) .

Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.

Probleem met Kampioen: https://github.com/dotnet/csharplang/issues/4934

Samenvatting

Voorgestelde wijzigingen:

  1. Lambdas met kenmerken toestaan
  2. Lambdas met expliciet retourtype toestaan
  3. Een natuurlijk delegatetype afleiden voor lambdas en methodegroepen

Motivatie

Ondersteuning voor kenmerken op lambdas zou pariteit bieden met methoden en lokale functies.

Ondersteuning voor expliciete retourtypen biedt symmetrie met lambda-parameters waar expliciete typen kunnen worden opgegeven. Het toestaan van expliciete retourtypen zou ook controle bieden over de prestaties van de compiler in geneste lambdas, waarbij overbelastingsresolutie de lambda-body momenteel moet binden om de handtekening te bepalen.

Een natuurlijk type voor lambda-expressies en methodegroepen biedt meer scenario's waarin lambdas- en methodegroepen kunnen worden gebruikt zonder een expliciet gemachtigdentype, waaronder als initialisatiefunctie in var declaraties.

Het vereisen van expliciete gedelegeerdentypen voor zowel lambdas als methodegroepen vormt voor klanten een knelpunt en is een belemmering geworden voor de voortgang van ASP.NET met recente werkzaamheden aan MapAction.

ASP.NET MapAction- zonder voorgestelde wijzigingen (MapAction() gebruikt een System.Delegate argument):

[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- met natuurlijke typen voor methodegroepen:

[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction(GetTodo);

[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction(PostTodo);

ASP.NET MapAction- met kenmerken en natuurlijke typen voor lambda-expressies:

app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo);

Kenmerken

Kenmerken kunnen worden toegevoegd aan lambda-expressies en lambda-parameters. Om dubbelzinnigheid tussen methodekenmerken en parameterkenmerken te voorkomen, moet een lambda-expressie met kenmerken een lijst met haakjes gebruiken. Parametertypen zijn niet vereist.

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

Er kunnen meerdere kenmerken worden opgegeven, gescheiden door komma's in dezelfde kenmerklijst of als afzonderlijke kenmerklijsten.

var f = [A1, A2][A3] () => { };    // ok
var g = ([A1][A2, A3] int x) => x; // ok

Kenmerken worden niet ondersteund voor anonieme methoden gedeclareerd met delegate { } syntaxis.

f = [A] delegate { return 1; };         // syntax error at 'delegate'
f = delegate ([A] int x) { return x; }; // syntax error at '['

De parser kijkt vooruit om een initialisatiefunctie voor verzamelingen te onderscheiden met een elementtoewijzing van een initialisatiefunctie voor verzamelingen met een lambda-expressie.

var y = new C { [A] = x };    // ok: y[A] = x
var z = new C { [A] x => x }; // ok: z[0] = [A] x => x

De parser behandelt ?[ als het begin van toegang tot voorwaardelijke elementen.

x = b ? [A];               // ok
y = b ? [A] () => { } : z; // syntax error at '('

Kenmerken van de lambda-expressie of lambda-parameters worden toegevoegd aan de metagegevens van de methode die overeenkomt met de lambda.

Over het algemeen moeten gebruikers niet afhankelijk zijn van de manier waarop lambda-expressies en lokale functies worden toegewezen van bron naar metagegevens. Hoe lambdas en lokale functies worden verzonden, kunnen en zijn gewijzigd tussen compilerversies.

De hier voorgestelde wijzigingen zijn gericht op het Delegate georiënteerde scenario. Het moet geldig zijn om de MethodInfo te inspecteren die aan een Delegate-exemplaar is gekoppeld om de signatuur van de lambda-expressie of lokale functie te bepalen, inclusief expliciete attributen en aanvullende metagegevens die door de compiler worden gegenereerd, zoals standaardparameters. Hierdoor kunnen teams zoals ASP.NET hetzelfde gedrag beschikbaar maken voor lambdas en lokale functies als gewone methoden.

Expliciet retourtype

Een expliciet retourtype kan worden opgegeven vóór de lijst met haakjes geplaatste parameters.

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?

De parser kijkt vooruit om onderscheid te maken tussen een methode-aanroep T() van een lambda-expressie T () => e.

Expliciete retourtypen worden niet ondersteund voor anonieme methoden die zijn gedeclareerd met delegate { } syntaxis.

f = delegate int { return 1; };         // syntax error
f = delegate int (int x) { return x; }; // syntax error

Inferentie van het methodetype moet een exacte inferentie maken uit een expliciet lambda-retourtype.

static void F<T>(Func<T, T> f) { ... }
F(int (i) => i); // Func<int, int>

Variantieconversies zijn niet toegestaan van het retourtype van een lambda naar het retourtype van een delegate, zoals vergelijkbaar gedrag voor parametertypen.

Func<object> f1 = string () => null; // error
Func<object?> f2 = object () => x;   // warning

De parser staat toe dat lambda-expressies met ref retourtypen binnen expressies worden gebruikt zonder extra haakjes.

d = ref int () => x; // d = (ref int () => x)
F(ref int () => x);  // F((ref int () => x))

var kan niet worden gebruikt als een expliciet retourtype voor lambda-expressies.

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

Natuurlijk functietype

Een anonieme functie expressie (§12.19) (een lambda-expressie of een anonieme methode) heeft een natuurlijk type als de parameters expliciet zijn en het retourtype expliciet is of kan worden afgeleid (zie §12.6.3.13).

Een methodegroep een natuurlijk type heeft als alle kandidaatmethoden in de methodegroep een gemeenschappelijke handtekening hebben. (Als de methodegroep uitbreidingsmethoden kan bevatten, bevatten de kandidaten het bijbehorende type en alle uitbreidingsmethodebereiken.)

Het natuurlijke type van een anonieme functie-expressie of methodegroep is een function_type. Een function_type geeft een methodesignatuur weer: de parametertypen en ref-typen, en het retourtype en ref-type. Anonieme functie-expressies of methodegroepen met dezelfde handtekening hebben dezelfde function_type.

Function_types worden alleen in een paar specifieke contexten gebruikt:

  • impliciete en expliciete conversies
  • methodetype-inferentie (§12.6.3) en beste gemeenschappelijke type (§12.6.3.15)
  • var initialisatoren

Er bestaat alleen een function_type tijdens het compileren: function_types worden niet weergegeven in de bron of metagegevens.

Conversies

Vanuit een function_typeF zijn er impliciete function_type conversies:

  • Voor een function_typeG, als de parameters en retourtypen van F variantie-converteerbaar zijn naar de parameters en retourtypen van G
  • System.MulticastDelegate of basisklassen of interfaces van System.MulticastDelegate
  • System.Linq.Expressions.Expression of System.Linq.Expressions.LambdaExpression

Anonieme functie-expressies en methodegroepen hebben al conversies van expressies om typen en expressiestructuurtypen te delegeren (zie anonieme functieconversies §10,7 en methodegroepconversies §10,8). Deze conversies zijn voldoende voor conversie naar sterk getypeerde gedelegeerdentypen en expressiestructuurtypen. De bovengenoemde function_type conversies voegen alleen conversies van het type toe aan de basistypen: System.MulticastDelegate, System.Linq.Expressions.Expression, enzovoort.

Er zijn geen conversies naar een function_type van een ander type dan een function_type. Er zijn geen expliciete conversies voor function_types omdat er niet naar function_types kan worden verwezen in de bron.

Een conversie naar System.MulticastDelegate of basistype of -interface realiseert de anonieme functie of methodegroep als een exemplaar van een geschikt gemachtigdetype. Een conversie naar System.Linq.Expressions.Expression<TDelegate> of basistype realiseert de lambda-expressie als een expressiestructuur met een geschikt gemachtigdetype.

Delegate d = delegate (object obj) { }; // Action<object>
Expression e = () => "";                // Expression<Func<string>>
object o = "".Clone;                    // Func<object>

Function_type conversies zijn geen impliciete of expliciete standaardconversies §10,4 en worden niet meegenomen bij het bepalen of een door de gebruiker gedefinieerde conversieoperator van toepassing is op een anonieme functie of methodegroep. Van evaluatie van door de gebruiker gedefinieerde conversies §10.5.3:

Om een conversieoperator van toepassing te kunnen zijn, moet het mogelijk zijn om een standaardconversie uit te voeren (§10,4) van het brontype naar het operandtype van de operator, en moet het mogelijk zijn om een standaardconversie uit te voeren van het resultaattype van de operator naar het doeltype.

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'

Er wordt een waarschuwing gerapporteerd voor een impliciete conversie van een methodegroep naar object, omdat de conversie geldig is, maar mogelijk onbedoeld.

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

Type inferentie

De bestaande regels voor typedeductie zijn meestal ongewijzigd (zie §12.6.3). Er zijn echter enkele wijzigingen hieronder in specifieke fasen van type-inferentie.

Eerste fase

In de eerste fase (§12.6.3.2) kan een anonieme functie worden verbonden met Ti, zelfs als Ti geen type gedelegeerde of expressiestructuurtype is (mogelijk een typeparameter beperkt tot System.Delegate bijvoorbeeld).

Voor elk van de methodeargumenten Ei:

  • Als Ei een anonieme functie is en Ti een gemachtigde type of expressiestructuurtype is, wordt een expliciete parametertypedeductie gemaakt van Ei tot Tien wordt een expliciete retourtypedeductie gemaakt van Ei tot Ti.
  • Anders, als Ei een type U heeft en xi een waardeparameter is, wordt een ondergrensdeductie gemaakt vanUtotTi.
  • Anders, als Ei een type U heeft en xi een ref of out parameter is, wordt een exacte redenering gemaakt vanUnaarTi.
  • Anders wordt er geen deductie gemaakt voor dit argument.

Expliciete retourtype-inferentie

Een expliciete retourtypedeductie wordt gemaakt van een expressie Eom een type T op de volgende manier te:

  • Als E een anonieme functie is met expliciet retourtype Ur en T een type gedelegeerde of expressiestructuur is met retourtype Vr, wordt er een exacte deductie (§12.6.3.9) gemaakt vanUrtotVr.

Repareren

Het oplossen (§12.6.3.12) zorgt ervoor dat andere conversies de voorkeur krijgen boven function_type conversies. (Lambda-expressies en methodegroepexpressies dragen alleen bij aan ondergrenzen, zodat de verwerking van function_types alleen nodig is voor ondergrenzen.)

Een niet-opgeloste typevariabele Xi met een reeks beperkingen wordt als volgt vastgesteld:

  • De set kandidaattypenUj begint als de verzameling van alle typen binnen de grenzen voor Xi, waarbij functietypen in de lagere grenzen worden genegeerd als er typen zijn die geen functietypen zijn.
  • Vervolgens onderzoeken we elke grens voor Xi op zijn beurt: voor elke exacte grens U van Xi worden alle typen Uj die niet identiek zijn aan U uit de kandidaatset verwijderd. Voor elke ondergrens U van Xi worden alle typen Uj waarvoor er geen impliciete conversie van U is uit de kandidaatset verwijderd. Voor elke bovengrens U van Xi alle typen Uj waarvan er geen impliciete conversie naar U worden verwijderd uit de kandidaatset.
  • Als er onder de overige kandidaattypen Uj een uniek type V is dat een impliciete conversie naar alle andere kandidaattypen heeft, wordt Xi vastgezet op V.
  • Anders mislukt de type-afleiding.

Meest gangbaar type

Het beste gangbare type (§12.6.3.15) wordt gedefinieerd in termen van type-afleiding, zodat de hierboven genoemde wijzigingen in type-afleiding ook van toepassing zijn op het beste gangbare type.

var fs = new[] { (string s) => s.Length, (string s) => int.Parse(s) }; // Func<string, int>[]

var

Anonieme functies en methodegroepen met functietypen kunnen worden gebruikt als initialisatiefuncties in var declaraties.

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> 

Functietypen worden niet gebruikt in toekenningen aan ongebruikte waarden.

d = () => 0; // ok
_ = () => 1; // error

Typen gemachtigden

Het gemachtigde type voor de anonieme functie of methodegroep met parametertypen P1, ..., Pn en retourtype R is:

  • als een parameter of retourwaarde niet op waarde is, of als er meer dan 16 parameters zijn, of een van de parametertypen of return ongeldige typeargumenten zijn (bijvoorbeeld (int* p) => { }), is de gemachtigde een gesynthetiseerd internal anoniem gemachtigdetype met handtekening die overeenkomt met de anonieme functie of methodegroep, en met parameternamen arg1, ..., argn of arg als één parameter;
  • als R gelijk is aan void, dan is het delegatietype System.Action<P1, ..., Pn>;
  • anders is het delegatetype System.Func<P1, ..., Pn, R>.

De compiler kan in de toekomst meer signaturen toestaan om te binden aan System.Action<> en System.Func<> typen (als ref struct typen bijvoorbeeld type-argumenten zijn toegestaan).

modopt() of modreq() in de signatuur van de methodegroep worden genegeerd in het bijbehorende delegate-type.

Als voor twee anonieme functies of methodegroepen in dezelfde compilatie gesynthetiseerde gedelegeerdentypen met dezelfde parametertypen en modifiers en hetzelfde retourtype en dezelfde wijzigingstype zijn vereist, gebruikt de compiler hetzelfde gesynthetiseerde gemachtigdetype.

Overbelastingsresolutie

Beter functielid (§12.6.4.3) wordt bijgewerkt om de voorkeur te geven aan leden waarbij geen van de conversies en geen van de typeargumenten betrokken afgeleide typen van lambda-expressies of methodegroepen zijn.

Beter functielid

... Gezien een lijst met argumenten A met een set argumentexpressies {E1, E2, ..., En} en twee toepasselijke functieleden Mp en Mq met parametertypen {P1, P2, ..., Pn} en {Q1, Q2, ..., Qn}, wordt Mp gedefinieerd als een beter functielid dan Mq als

  1. voor elk argument is de impliciete conversie van Ex naar Px geen functie-typeconversieen
    • Mp een niet-algemene methode is of Mp een algemene methode is met typeparameters {X1, X2, ..., Xp} en voor elke typeparameter Xi wordt het typeargument afgeleid van een expressie of een ander type dan een function_typeen
    • Voor ten minste één argument is de impliciete conversie van Ex naar Qx een functie_type_conversie, of Mq is een generieke methode met typeparameters {Y1, Y2, ..., Yq}, en voor ten minste één typeparameter Yi wordt het typeargument afgeleid van een functie_type, of.
  2. voor elk argument is de impliciete conversie van Ex naar Qx niet beter dan de impliciete conversie van Ex naar Px, en voor ten minste één argument is de conversie van Ex naar Px beter dan de conversie van Ex naar Qx.

Een betere conversie van expressie (§12.6.4.5) wordt bijgewerkt om de voorkeur te geven aan conversies waarbij geen afgeleide typen van lambda-expressies of methodegroepen zijn betrokken.

Betere conversie van uitdrukking

Gezien een impliciete conversie C1 die wordt geconverteerd van een expressie E naar een type T1en een impliciete conversie C2 die wordt geconverteerd van een expressie E naar een type T2, is C1 een betere conversie dan C2 als:

  1. C1 is geen function_type_conversion en C2 is een function_type_conversionof
  2. E is een niet-constante interpolated_string_expression, C1 is een implicit_string_handler_conversion, T1 is een applicable_interpolated_string_handler_type, en C2 is geen implicit_string_handler_conversion, of
  3. E komt niet exact overeen met T2 en ten minste één van de volgende geldt:

Syntaxis

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?
  ;

Openstaande problemen

Moeten standaardwaarden worden ondersteund voor lambda-expressieparameters voor volledigheid?

Moet System.Diagnostics.ConditionalAttribute niet zijn toegestaan voor lambda-expressies, omdat er weinig scenario's zijn waarin een lambda-expressie voorwaardelijk kan worden gebruikt?

([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?

Moet de function_type beschikbaar zijn vanuit de compiler-API, naast het resulterende gemachtigdentype?

Op dit moment gebruikt het type uitgestelde gemachtigde System.Action<> of System.Func<> wanneer parameter- en retourtypen geldige typeargumenten zijn en er niet meer dan 16 parameters zijn en als het verwachte Action<> of Func<> type ontbreekt, wordt er een fout gerapporteerd. Moet de compiler in plaats daarvan ongeacht de ariteit System.Action<> of System.Func<> gebruiken? En als het verwachte type ontbreekt,synthetiseert u anders een gemachtigdentype?