Dela via


Lambda-förbättringar

Not

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader samlas in i de relevanta anteckningarna från LDM (Language Design Meeting) .

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Champion-problem: https://github.com/dotnet/csharplang/issues/4934

Sammanfattning

Föreslagna ändringar:

  1. Tillåt lambdas med attribut
  2. Tillåt lambdas med explicit returtyp
  3. Härled en naturlig delegattyp för lambdas och metodgrupper

Motivation

Stöd för attribut på lambdas skulle ge paritet med metoder och lokala funktioner.

Stöd för explicita returtyper skulle ge symmetri med lambda-parametrar där explicita typer kan anges. Att tillåta explicita returtyper skulle också ge kontroll över kompilatorns prestanda i kapslade lambdas där överbelastningsupplösning måste binda lambda-brödtexten för närvarande för att fastställa signaturen.

En naturlig typ för lambda-uttryck och metodgrupper tillåter fler scenarier där lambdas och metodgrupper kan användas utan en explicit delegattyp, inklusive som initialiserare i var-deklarationer.

Att kräva explicita delegattyper för lambdas och metodgrupper har varit en friktionspunkt för kunder och har blivit ett hinder för att gå vidare i ASP.NET med det senaste arbetet med MapAction.

ASP.NET MapAction utan föreslagna ändringar (MapAction() tar ett 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 med naturliga typer för metodgrupper:

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

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

ASP.NET MapAction- med attribut och naturliga typer för lambda-uttryck:

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

Attribut

Attribut kan läggas till i lambda-uttryck och lambda-parametrar. För att undvika tvetydighet mellan metodattribut och parameterattribut måste ett lambda-uttryck med attribut använda en parentesiserad parameterlista. Parametertyper krävs inte.

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

Flera attribut kan anges, antingen kommaavgränsade i samma attributlista eller som separata attributlistor.

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

Attributen stöds inte för de anonyma metoder deklarerade med delegate { } syntax.

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

Parsern ser framåt för att särskilja en insamlingsinitierare med en elementtilldelning från en samlingsinitierare med ett lambda-uttryck.

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

Parsern behandlar ?[ som början på en villkorlig elementåtkomst.

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

Attribut för lambda-uttrycken eller lambda-parametrarna kommer att genereras som metadata på metoden som motsvarar lambda.

I allmänhet bör kunderna inte vara beroende av hur lambda-uttryck och lokala funktioner mappas från källa till metadata. Hur lambdas och lokala funktioner genereras kan och har ändrats mellan kompilatorversioner.

De ändringar som föreslås här är inriktade på det Delegate drivna scenariot. Det bör vara giltigt att inspektera MethodInfo som är associerad med en Delegate-instans för att fastställa signaturen för lambda-uttrycket eller den lokala funktionen, inklusive explicita attribut och ytterligare metadata som avges av kompilatorn, såsom standardparametrar. På så sätt kan team som ASP.NET göra samma beteenden tillgängliga för lambdas och lokala funktioner som vanliga metoder.

Explicit returtyp

En explicit returtyp kan anges före den parentesiserade parameterlistan.

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?

Parsern ser framåt för att skilja ett metodanrop T() från ett lambda-uttryck T () => e.

Explicita returtyper stöds inte för anonyma metoder som deklareras med delegate { } syntax.

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

Metodtypinferens bör göra en exakt slutsats från en explicit lambda-returtyp.

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

Variationskonverteringar tillåts inte från lambda-returtyp till delegerad returtyp, som matchar liknande beteende för parametertyper.

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

Parsersn tillåter lambda-uttryck med ref-returtyper inom uttryck utan extra parenteser.

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

var kan inte användas som explicit returtyp för lambda-uttryck.

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

Naturlig funktionstyp

En anonym funktion uttryck (§12.19) (ett lambda-uttryck eller en anonym metod) har en naturlig typ om parametrarna är explicita och returtypen antingen är explicit eller kan härledas (se §12.6.3.13).

En -metodgrupp har en naturlig typ om alla kandidatmetoder i metodgruppen har en gemensam signatur. (Om metodgruppen kan innehålla tilläggsmetoder, innehåller kandidaterna den innehållande typen och alla omfång för tilläggsmetoderna.)

Den naturliga typen av ett anonymt funktionsuttryck eller en metodgrupp är en function_type. En function_type representerar en metodsignatur: parametertyperna och referenstyperna samt returtyp och referenstyp. Anonyma funktionsuttryck eller metodgrupper med samma signatur har samma function_type.

Function_types används endast i några få specifika sammanhang:

  • implicita och explicita konverteringar
  • metodtypsinferens (§12.6.3) och bästa vanliga typ (§12.6.3.15)
  • var initierare

En function_type finns endast vid kompileringstid: function_types visas inte i källan eller metadata.

Omvandlingar

Från en function_typeF finns implicita function_type konverteringar:

  • Till en function_typeG om parametrarna och returtyperna för F är varianskombinerade till parametrarna och returtypen för G
  • Alternativ System.MulticastDelegate eller basklasser eller gränssnitt för System.MulticastDelegate
  • Till System.Linq.Expressions.Expression eller System.Linq.Expressions.LambdaExpression

Anonyma funktionsuttryck och metodgrupper har redan konverteringar från uttryck för att delegera typer och uttrycksträdstyper (se anonyma funktionskonverteringar §10.7 och metodgruppskonverteringar §10.8). Dessa konverteringar är tillräckliga för att omvandla till starkt skrivna delegattyper och uttrycksträd. De function_type konverteringarna ovan lägger till konverteringar från typ endast till bastyperna: System.MulticastDelegate, System.Linq.Expressions.Expressionosv.

Det finns inga konverteringar till en function_type från en annan typ än en function_type. Det finns inga explicita konverteringar för function_types eftersom function_types inte kan refereras i källan.

En konvertering till System.MulticastDelegate eller bastyp eller gränssnitt förverkligar den anonyma funktionen eller metodgruppen som en instans av en lämplig delegattyp. En konvertering till System.Linq.Expressions.Expression<TDelegate> eller bastyp förverkligar lambda-uttrycket som ett uttrycksträd med en lämplig delegattyp.

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

Function_type konverteringar är inte implicita eller explicita standardkonverteringar §10.4 och beaktas inte när du fastställer om en användardefinierad konverteringsoperator ska tillämpas på en anonym funktion eller metodgrupp. Från utvärdering av användardefinierade konverteringar §10.5.3:

För att en konverteringsoperatör ska vara tillämplig måste det vara möjligt att utföra en standardkonvertering (§10.4) från källtypen till operatorns operandtyp, och det måste vara möjligt att utföra en standardkonvertering från operatörens resultattyp till måltypen.

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'

En varning rapporteras för en implicit konvertering av en metodgrupp till objecteftersom konverteringen är giltig men kanske oavsiktlig.

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

Typinferens

De befintliga reglerna för typinferens är i huvudsak oförändrade (se §12.6.3). Det finns dock ett par ändringar nedan för specifika faser av typinferens.

Första fasen

I den första fasen (§12.6.3.2) kan en anonym funktion binda till Ti även om Ti inte är en delegat- eller uttrycksträdstyp (kanske en typparameter som är begränsad till System.Delegate till exempel).

För vart och ett av metodargumenten Ei:

  • Om Ei är en anonym funktion och Ti är en trädtyp för ombud eller uttryckgörs en explicit parametertypsinferens från Ei till Tioch en explicit slutsatsdragning av returtyp görs från Ei till Ti.
  • Om Ei har en typ U och xi är en värdeparameter görs annars en med lägre bindning frånUtillTi.
  • Om Ei annars har en typ U och xi är en ref- eller out-parameter görs en exakt slutsatsdragningfrånUtillTi.
  • Annars görs ingen slutsatsdragning för det här argumentet.

Explicit slutsatsdragning av returtyp

En explicit returtypsinferens görs från ett uttryck Etill en typ T på följande sätt:

  • Om E är en anonym funktion med explicit returtyp Ur och T är en delegattyp eller uttrycksträdstyp med returtyp Vr görs en exakt slutsatsdragning (§12.6.3.9) frånUrtillVr.

Åtgärdande

Fixering (§12.6.3.12) säkerställer att andra konverteringar föredras framför function_type konverteringar. (Lambda-uttryck och metodgrupputtryck bidrar bara till lägre gränser, så hanteringen av function_types behövs endast för lägre gränser.)

En variabel av typen Xi med en uppsättning gränser är fast på följande sätt:

  • Mängden av kandidattyperUj börjar som mängden av alla typer i mängden av gränser för Xidär funktionstyper ignoreras i de lägre gränserna om det finns några typer som inte är funktionstyper.
  • Vi undersöker sedan varje bindning för Xi i tur och ordning: För varje exakt bunden U av Xi tas alla typer Uj som inte är identiska med U bort från kandidatuppsättningen. För varje lägre bunden U av Xi alla typer Uj som det finns inte en implicit konvertering från U tas bort från kandidatuppsättningen. För varje övre gräns U av Xi, tas alla typer Uj bort från kandidatuppsättningen som det inte finns någon implicit konvertering till U från .
  • Om det bland de återstående kandidattyperna Uj finns en unik typ V där det finns en implicit konvertering till alla andra kandidattyper, är Xi fast till V.
  • Annars misslyckas typinferensen.

Bästa vanliga typ

Bästa vanliga typ (§12.6.3.15) definieras i termer av typinferens så att typinferensändringarna ovan även gäller för bästa vanliga typ.

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

var

Anonyma funktioner och metodgrupper med funktionstyper kan användas som initierare i var deklarationer.

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> 

Funktionstyper används inte i tilldelningar för att ta bort.

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

Delegattyper

Ombudstypen för den anonyma funktionen eller metodgruppen med parametertyper P1, ..., Pn och returtyp R är:

  • om någon parameter eller returvärde inte är ett värde, eller om det finns fler än 16 parametrar, eller om någon av parametertyperna eller returen inte är giltiga typargument (till exempel (int* p) => { }), så är delegatet en syntetiserad internal anonym delegattyp vars signatur matchar den anonyma funktionen eller metodgruppen, och med parameternamnen arg1, ..., argn eller arg för enskilda parametrar.
  • om R är void, då är ombudstypen System.Action<P1, ..., Pn>.
  • annars är ombudstypen System.Func<P1, ..., Pn, R>.

Kompilatorn kan i framtiden tillåta att fler signaturer binder sig till typerna System.Action<> och System.Func<> (om ref struct-typer tillåts som typargument, till exempel).

modopt() eller modreq() i metodgruppens signatur ignoreras i motsvarande delegattyp.

Om två anonyma funktioner eller metodgrupper i samma kompilering kräver syntetiserade ombudstyper med samma parametertyper och modifierare och samma returtyp och modifierare använder kompilatorn samma syntetiserade ombudstyp.

Överbelastningsupplösning

Bättre funktionsmedlem (§12.6.4.3) uppdateras för att föredra medlemmar där varken några av konverteringarna eller typargumenten bygger på härledda typer från lambda-uttryck eller metodgrupper.

Bättre funktionsmedlem

... Med tanke på en argumentlista A med en uppsättning argumentuttryck {E1, E2, ..., En} och två tillämpliga funktionsmedlemmar Mp och Mq med parametertyper {P1, P2, ..., Pn} och {Q1, Q2, ..., Qn}, definieras Mp som en bättre funktionsmedlem än Mq om

  1. för varje argument är den implicita konverteringen från Ex till Px inte en funktionskonverteringstypoch
    • Mp är en icke-generisk metod eller Mp är en allmän metod med typparametrar {X1, X2, ..., Xp} och för varje typparameter Xi härleds typargumentet från ett uttryck eller från en annan typ än en function_typeoch
    • för minst ett argument är den implicita konverteringen från Ex till Qx en function_type_conversion, eller Mq är en allmän metod med typparametrar {Y1, Y2, ..., Yq} och för minst en typparameter Yi typargumentet härleds från en function_typeeller
  2. för varje argument är den implicita konverteringen från Ex till Qx inte bättre än den implicita konverteringen från Ex till Px, och för minst ett argument är konverteringen från Ex till Px bättre än konverteringen från Ex till Qx.

Bättre konvertering från uttryck (§12.6.4.5) uppdateras för att föredra konverteringar som inte omfattade härledda typer från lambda-uttryck eller metodgrupper.

Bättre omvandling av uttryck

Med en implicit konvertering C1 som konverterar från ett uttryck E till en typ T1och en implicit konvertering C2 som konverteras från ett uttryck E till en typ T2är C1 en bättre konvertering än C2 om:

  1. C1 är inte en function_type_conversion och C2 är en function_type_conversioneller
  2. E är en icke-konstant interpolated_string_expression, och C1 är en implicit_string_handler_conversion, T1 är en applicable_interpolated_string_handler_type, och C2 är inte en implicit_string_handler_conversion, eller
  3. E matchar inte exakt T2 och minst något av följande gäller:

Syntax

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

Öppna problem

Bör standardvärden stödjas för lambda-uttrycksparametrar för fullständighet?

Bör System.Diagnostics.ConditionalAttribute inte tillåtas i lambda-uttryck eftersom det inte finns många scenarier där ett lambda-uttryck kan användas villkorligt?

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

Ska function_type vara tillgänglig från kompilator-API:et, utöver den resulterande delegattypen?

För närvarande använder den härledda delegattypen System.Action<> eller System.Func<> när parameter- och returtyper är giltiga typargument och det inte finns fler än 16 parametrar, och om den förväntade Action<> eller Func<> typen saknas rapporteras ett fel. Ska kompilatorn i stället använda System.Action<> eller System.Func<> oavsett aritet? Och om den förväntade typen saknas syntetiserar du annars en ombudstyp?