Valfria och parameterfältparametrar för lambdas och metodgrupper
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 fångas i de relevanta LDM (Language Design Meeting)-anteckningarna.
Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.
Champion-fråga: https://github.com/dotnet/csharplang/issues/6051
Sammanfattning
För att bygga vidare på lambda-förbättringarna som introducerades i C# 10 (se relevant bakgrund) föreslår vi att du lägger till stöd för standardparametervärden och params
matriser i lambdas. Detta skulle göra det möjligt för användare att implementera följande lambdas:
var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3
På samma sätt tillåter vi samma typ av beteende för metodgrupper:
var addWithDefault = AddWithDefaultMethod;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = CountMethod;
counter(); // 0
counter(1, 2); // 2
int AddWithDefaultMethod(int addTo = 2) {
return addTo + 1;
}
int CountMethod(params int[] xs) {
return xs.Length;
}
Relevant bakgrund
Metodgruppkonverteringsspecifikation §10.8
Motivation
Appramverk i .NET-ekosystemet utnyttjar lambdas kraftigt så att användarna snabbt kan skriva affärslogik som är associerad med en slutpunkt.
var app = WebApplication.Create(args);
app.MapPost("/todos/{id}", (TodoService todoService, int id, string task) => {
var todo = todoService.Create(id, task);
return Results.Created(todo);
});
Lambdas har för närvarande inte stöd för att ange standardvärden för parametrar, så om en utvecklare vill skapa ett program som var motståndskraftigt mot scenarier där användarna inte angav data, lämnas de att antingen använda lokala funktioner eller ange standardvärdena i lambda-brödtexten, i motsats till den mer kortfattade föreslagna syntaxen.
var app = WebApplication.Create(args);
app.MapPost("/todos/{id}", (TodoService todoService, int id, string task = "foo") => {
var todo = todoService.Create(id, task);
return Results.Created(todo);
});
Den föreslagna syntaxen har också fördelen att minska förvirrande skillnader mellan lambdas och lokala funktioner, vilket gör det lättare att resonera om konstruktioner och "växa upp" lambdas till funktioner utan att kompromissa med funktioner, särskilt i andra scenarier där lambdas används i API:er där metodgrupper också kan tillhandahållas som referenser.
Detta är också den främsta motivationen för att stödja den params
matris som inte omfattas av ovan nämnda användningsfallsscenario.
Till exempel:
var app = WebApplication.Create(args);
Result TodoHandler(TodoService todoService, int id, string task = "foo") {
var todo = todoService.Create(id, task);
return Results.Created(todo);
}
app.MapPost("/todos/{id}", TodoHandler);
Tidigare beteende
Innan C# 12, när en användare implementerar en lambda med en valfri eller params
parameter, genererar kompilatorn ett fel.
var addWithDefault = (int addTo = 2) => addTo + 1; // error CS1065: Default values are not valid in this context.
var counter = (params int[] xs) => xs.Length; // error CS1670: params is not valid in this context
När en användare försöker använda en metodgrupp där den underliggande metoden har en valfri parameter eller params
, sprids inte denna information, vilket gör att anropet till metoden inte genomgår typkontroll på grund av felaktigt antal argument.
void M1(int i = 1) { }
var m1 = M1; // Infers Action<int>
m1(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int>'
void M2(params int[] xs) { }
var m2 = M2; // Infers Action<int[]>
m2(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int[]>'
Nytt beteende
Efter det här förslaget (en del av C# 12) kan standardvärden och params
tillämpas på lambda-parametrar med följande beteende:
var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3
Standardvärden och params
kan tillämpas på metodgruppsparametrar genom att specifikt definiera en sådan metodgrupp:
int AddWithDefault(int addTo = 2) {
return addTo + 1;
}
var add1 = AddWithDefault;
add1(); // ok, default parameter value will be used
int Counter(params int[] xs) {
return xs.Length;
}
var counter1 = Counter;
counter1(1, 2, 3); // ok, `params` will be used
Icke-bakåtkompatibel ändring
Före C# 12 är den härledda typen av en metodgrupp Action
eller Func
så följande kod kompileras:
void WriteInt(int i = 0) {
Console.Write(i);
}
var writeInt = WriteInt; // Inferred as Action<int>
DoAction(writeInt, 3); // Ok, writeInt is an Action<int>
void DoAction(Action<int> a, int p) {
a(p);
}
int Count(params int[] xs) {
return xs.Length;
}
var counter = Count; // Inferred as Func<int[], int>
DoFunction(counter, 3); // Ok, counter is a Func<int[], int>
int DoFunction(Func<int[], int> f, int p) {
return f(new[] { p });
}
Efter den här ändringen (en del av C# 12) upphör den här typen av kod att kompileras i .NET SDK 7.0.200 eller senare.
void WriteInt(int i = 0) {
Console.Write(i);
}
var writeInt = WriteInt; // Inferred as anonymous delegate type
DoAction(writeInt, 3); // Error, cannot convert from anonymous delegate type to Action
void DoAction(Action<int> a, int p) {
a(p);
}
int Count(params int[] xs) {
return xs.Length;
}
var counter = Count; // Inferred as anonymous delegate type
DoFunction(counter, 3); // Error, cannot convert from anonymous delegate type to Func
int DoFunction(Func<int[], int> f, int p) {
return f(new[] { p });
}
De effekter som denna icke-bakåtkompatibla ändring har måste beaktas. Lyckligtvis har användningen av var
för att härleda typen av en metodgrupp endast stödts sedan C# 10, så endast kod som har skrivits sedan dess som uttryckligen förlitar sig på det här beteendet skulle brytas.
Detaljerad design
Grammatik- och parsningsändringar
Den här förbättringen kräver följande ändringar i grammatiken för lambda-uttryck.
lambda_expression
: modifier* identifier '=>' (block | expression)
- | attribute_list* modifier* type? lambda_parameters '=>' (block | expression)
+ | attribute_list* modifier* type? lambda_parameter_list '=>' (block | expression)
;
+lambda_parameter_list
+ : lambda_parameters (',' parameter_array)?
+ | parameter_array
+ ;
lambda_parameter
: identifier
- | attribute_list* modifier* type? identifier
+ | attribute_list* modifier* type? identifier default_argument?
;
Observera att detta endast tillåter standardparametervärden och params
matriser för lambdas, inte för anonyma metoder som deklarerats med delegate { }
syntax.
Samma regler som för metodparametrar (§15.6.2) gäller för lambda-parametrar:
- En parameter med en
ref
,out
ellerthis
-modifierare kan inte ha en default_argument. - En parameter_array kan inträffa efter en valfri parameter, men kan inte ha ett standardvärde – utelämnandet av argument för en parameter_array skulle i stället resultera i att en tom matris skapas.
Inga ändringar i grammatiken är nödvändiga för metodgrupper eftersom det här förslaget bara skulle ändra deras semantik.
Följande tillägg (i fetstil) krävs för anonyma funktionskonverteringar (§10.7):
Specifikt är en anonym funktion
F
kompatibel med en delegattypD
under förutsättning att:
- [...]
- Om
F
har en uttryckligen angiven parameterlista har varje parameter iD
samma typ och modifierare som motsvarande parameter iF
ignorerarparams
modifierare och standardvärden.
Uppdateringar av tidigare förslag
Följande tillägg (i fetstil) krävs för funktionstyper specifikation i ett tidigare förslag:
En -metodgrupp har en naturlig typ om alla kandidatmetoder i metodgruppen har en gemensam signatur inklusive standardvärden och
params
modifierare. (Om metodgruppen kan innehålla tilläggsmetoder innehåller kandidaterna den innehållande typen och alla omfång för tilläggsmetoden.)
Den naturliga typen av ett anonymt funktionsuttryck eller en metodgrupp är en function_type. En function_type representerar en metodsignatur: parametertyperna, standardvärden, referenstyper,
params
modifierareoch returtyp och referenstyp. Anonyma funktionsuttryck eller metodgrupper med samma signatur har samma function_type.
Följande tillägg (i fetstil) krävs för delegeringstyp-specifikationen i ett tidigare förslag:
Ombudstypen för den anonyma funktionen eller metodgruppen med parametertyper
P1, ..., Pn
och returtypR
är:
- Om någon parameter eller returvärde inte är efter värde, eller någon parameter är valfri eller
params
, eller om det finns fler än 16 parametrar, eller någon av parametertyperna eller returvärdet inte är giltiga typer för argument (till exempel(int* p) => { }
), är delegaten en syntetiseradinternal
anonym delegattyp vars signatur matchar den anonyma funktionen eller metodgruppen, och med parameternamnarg1, ..., argn
ellerarg
om en enskild parameter;
Binderändringar
Syntetisera nya ombudstyper
Precis som med beteendet för delegater med ref
eller out
parametrar, syntetiseras delegattyper för lambdas eller metodgrupper som definierats med valfria eller params
parametrar.
Observera att i exemplen nedan används notationen a'
, b'
osv. för att representera dessa anonyma ombudstyper.
var addWithDefault = (int addTo = 2) => addTo + 1;
// internal delegate int a'(int arg = 2);
var printString = (string toPrint = "defaultString") => Console.WriteLine(toPrint);
// internal delegate void b'(string arg = "defaultString");
var counter = (params int[] xs) => xs.Length;
// internal delegate int c'(params int[] arg);
string PathJoin(string s1, string s2, string sep = "/") { return $"{s1}{sep}{s2}"; }
var joinFunc = PathJoin;
// internal delegate string d'(string arg1, string arg2, string arg3 = " ");
Beteende för konvertering och enande
Anonyma ombud med valfria parametrar kommer att vara enhetliga när samma parameter (baserat på position) har samma standardvärde, oavsett parameternamn.
int E(int j = 13) {
return 11;
}
int F(int k = 0) {
return 3;
}
int G(int x = 13) {
return 4;
}
var a = (int i = 13) => 1;
// internal delegate int b'(int arg = 13);
var b = (int i = 0) => 2;
// internal delegate int c'(int arg = 0);
var c = (int i = 13) => 3;
// internal delegate int b'(int arg = 13);
var d = (int c = 13) => 1;
// internal delegate int b'(int arg = 13);
var e = E;
// internal delegate int b'(int arg = 13);
var f = F;
// internal delegate int c'(int arg = 0);
var g = G;
// internal delegate int b'(int arg = 13);
a = b; // Not allowed
a = c; // Allowed
a = d; // Allowed
c = e; // Allowed
e = f; // Not Allowed
b = f; // Allowed
e = g; // Allowed
d = (int c = 10) => 2; // Warning: default parameter value is different between new lambda
// and synthesized delegate b'. We won't do implicit conversion
Anonyma delegater med ett fält som sista parameter kommer att förenas när den sista parametern har samma params
-modifierare och fälttyp, oavsett vad parametern heter.
int C(int[] xs) {
return xs.Length;
}
int D(params int[] xs) {
return xs.Length;
}
var a = (int[] xs) => xs.Length;
// internal delegate int a'(int[] xs);
var b = (params int[] xs) => xs.Length;
// internal delegate int b'(params int[] xs);
var c = C;
// internal delegate int a'(int[] xs);
var d = D;
// internal delegate int b'(params int[] xs);
a = b; // Not allowed
a = c; // Allowed
b = c; // Not allowed
b = d; // Allowed
c = (params int[] xs) => xs.Length; // Warning: different delegate types; no implicit conversion
d = (int[] xs) => xs.Length; // OK. `d` is `delegate int (params int[] arg)`
På samma sätt finns det naturligtvis kompatibilitet med namngivna ombud som redan stöder valfria och params
parametrar.
När standardvärden eller params
modifierare skiljer sig åt i en konvertering, kommer källvärdet att vara oanvänd om det är i ett lambda-uttryck, eftersom lambda inte kan anropas på något annat sätt.
Det kan verka kontraintuitivt för användarna, därför genereras en varning när källans standardvärde eller params
modifierare finns och skiljer sig från målvärdet.
Om källan är en metodgrupp kan den anropas på egen hand. Därför genereras ingen varning.
delegate int DelegateNoDefault(int x);
delegate int DelegateWithDefault(int x = 1);
int MethodNoDefault(int x) => x;
int MethodWithDefault(int x = 2) => x;
DelegateNoDefault d1 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d2 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d3 = MethodNoDefault; // no warning: source is a method group
DelegateNoDefault d4 = (int x = 1) => x; // warning: source present, target missing
DelegateWithDefault d5 = (int x = 2) => x; // warning: source present, target different
DelegateWithDefault d6 = (int x) => x; // no warning: source missing, target present
delegate int DelegateNoParams(int[] xs);
delegate int DelegateWithParams(params int[] xs);
int MethodNoParams(int[] xs) => xs.Length;
int MethodWithParams(params int[] xs) => xs.Length;
DelegateNoParams d7 = MethodWithParams; // no warning: source is a method group
DelegateWithParams d8 = MethodNoParams; // no warning: source is a method group
DelegateNoParams d9 = (params int[] xs) => xs.Length; // warning: source present, target missing
DelegateWithParams d10 = (int[] xs) => xs.Length; // no warning: source missing, target present
IL/körningsbeteende
Standardparametervärdena genereras till metadata. IL för den här funktionen kommer att vara mycket lik den IL som genereras för lambdas med ref
och out
parametrar. En klass som ärver från System.Delegate
eller liknande genereras, och metoden Invoke
innehåller .param
direktiv för att ange standardparametervärden eller System.ParamArrayAttribute
– precis som för en standard med namnet delegate med valfria parametrar eller params
parametrar.
Dessa delegetyper kan inspekteras vid körning som vanligt.
I kod kan användarna granska DefaultValue
i ParameterInfo
som är associerad med lambda- eller metodgruppen, genom att använda den associerade MethodInfo
.
var addWithDefault = (int addTo = 2) => addTo + 1;
int AddWithDefaultMethod(int addTo = 2)
{
return addTo + 1;
}
var defaultParm = addWithDefault.Method.GetParameters()[0].DefaultValue; // 2
var add1 = AddWithDefaultMethod;
defaultParm = add1.Method.GetParameters()[0].DefaultValue; // 2
Öppna frågor
Ingen av dessa har genomförts. De är fortfarande öppna förslag.
Öppen fråga: hur interagerar detta med det befintliga DefaultParameterValue
-attributet?
Föreslaget svar: För paritet tillåter du attributet DefaultParameterValue
på lambdas och ser till att beteendet för ombudsgenerering matchar standardparametervärden som stöds via syntaxen.
var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed
Öppen fråga: För det första, observera att detta ligger utanför det nuvarande förslagets räckvidd, men det kan vara värt att diskutera i framtiden. Vill vi ha stöd för standardvärden med implicit skrivna lambda-parametrar? D.v.s.
delegate void M1(int i = 3);
M1 m = (x = 3) => x + x; // Ok
delegate void M2(long i = 2);
M2 m = (x = 3.0) => ...; //Error: cannot convert implicitly from long to double
Den här slutsatsdragningen leder till några knepiga konverteringsproblem som skulle kräva mer diskussion.
Det finns också prestandaöverväganden för parsning här. I dag kan till exempel termen (x =
aldrig bli början på ett lambda-uttryck. Om den här syntaxen tilläts för lambda-standardvärden skulle parsern behöva en större lookahead (genomsöka hela vägen tills en =>
token) för att avgöra om en term är en lambda eller inte.
Designa möten
-
LDM 2022-10-10: beslut om att lägga till stöd för
params
på samma sätt som standardparametervärden.
C# feature specifications