Dela via


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

Lambda-förbättringar i C# 10

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 eller this-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 delegattyp D under förutsättning att:

  • [...]
  • Om F har en uttryckligen angiven parameterlista har varje parameter i D samma typ och modifierare som motsvarande parameter i Fignorerar params 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 returtyp R ä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 syntetiserad internal anonym delegattyp vars signatur matchar den anonyma funktionen eller metodgruppen, och med parameternamn arg1, ..., argn eller arg 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.