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:
- Tillåt lambdas med attribut
- Tillåt lambdas med explicit returtyp
- 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_type
G
om parametrarna och returtyperna förF
är varianskombinerade till parametrarna och returtypen förG
- Alternativ
System.MulticastDelegate
eller basklasser eller gränssnitt förSystem.MulticastDelegate
- Till
System.Linq.Expressions.Expression
ellerSystem.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.Expression
osv.
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 object
eftersom 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 ochTi
är en trädtyp för ombud eller uttryckgörs en explicit parametertypsinferens frånEi
tillTi
och en explicit slutsatsdragning av returtyp görs frånEi
tillTi
.- Om
Ei
har en typU
ochxi
är en värdeparameter görs annars en med lägre bindning frånU
tillTi
.- Om
Ei
annars har en typU
ochxi
är enref
- ellerout
-parameter görs en exakt slutsatsdragningfrånU
tillTi
.- Annars görs ingen slutsatsdragning för det här argumentet.
Explicit slutsatsdragning av returtyp
En explicit returtypsinferens görs från ett uttryck
E
till en typT
på följande sätt:
- Om
E
är en anonym funktion med explicit returtypUr
ochT
är en delegattyp eller uttrycksträdstyp med returtypVr
görs en exakt slutsatsdragning (§12.6.3.9) frånUr
tillVr
.
Å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 kandidattyper
Uj
börjar som mängden av alla typer i mängden av gränser förXi
dä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 bundenU
avXi
tas alla typerUj
som inte är identiska medU
bort från kandidatuppsättningen. För varje lägre bundenU
avXi
alla typerUj
som det finns inte en implicit konvertering frånU
tas bort från kandidatuppsättningen. För varje övre gränsU
avXi
, tas alla typerUj
bort från kandidatuppsättningen som det inte finns någon implicit konvertering tillU
från .- Om det bland de återstående kandidattyperna
Uj
finns en unik typV
där det finns en implicit konvertering till alla andra kandidattyper, ärXi
fast tillV
.- 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 syntetiseradinternal
anonym delegattyp vars signatur matchar den anonyma funktionen eller metodgruppen, och med parameternamnenarg1, ..., argn
ellerarg
för enskilda parametrar. - om
R
ärvoid
, då är ombudstypenSystem.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 funktionsmedlemmarMp
ochMq
med parametertyper{P1, P2, ..., Pn}
och{Q1, Q2, ..., Qn}
, definierasMp
som en bättre funktionsmedlem änMq
om
- för varje argument är den implicita konverteringen från
Ex
tillPx
inte en funktionskonverteringstypoch
Mp
är en icke-generisk metod ellerMp
är en allmän metod med typparametrar{X1, X2, ..., Xp}
och för varje typparameterXi
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
tillQx
en function_type_conversion, ellerMq
är en allmän metod med typparametrar{Y1, Y2, ..., Yq}
och för minst en typparameterYi
typargumentet härleds från en function_typeeller- för varje argument är den implicita konverteringen från
Ex
tillQx
inte bättre än den implicita konverteringen frånEx
tillPx
, och för minst ett argument är konverteringen frånEx
tillPx
bättre än konverteringen frånEx
tillQx
.
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 uttryckE
till en typT1
och en implicit konverteringC2
som konverteras från ett uttryckE
till en typT2
ärC1
en bättre konvertering änC2
om:
C1
är inte en function_type_conversion ochC2
är en function_type_conversionellerE
är en icke-konstant interpolated_string_expression, ochC1
är en implicit_string_handler_conversion,T1
är en applicable_interpolated_string_handler_type, ochC2
är inte en implicit_string_handler_conversion, ellerE
matchar inte exaktT2
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?
C# feature specifications