Sdílet prostřednictvím


Volitelné parametry pole parametrů pro lambda a skupiny metod

Poznámka

Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.

Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v příslušných poznámkách z jednání o návrhu jazyka (LDM).

Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .

Shrnutí

Abychom mohli stavět na vylepšeních lambda zavedených v jazyce C# 10 (viz relevantní pozadí ), navrhujeme přidání podpory výchozích hodnot parametrů a polí params v lambdách. To by uživatelům umožnilo implementovat následující lambda:

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

Podobně povolíme stejné chování pro skupiny metod:

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

Relevantní pozadí

Vylepšení Lambda v jazyce C# 10

specifikace převodu skupiny metod §10.8

Motivace

Architektury aplikací v ekosystému .NET využívají lambda, které uživatelům umožňují rychle psát obchodní logiku přidruženou ke koncovému bodu.

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);
});

Lambda v současné době nepodporují nastavení výchozích hodnot parametrů, takže pokud vývojář chtěl vytvořit aplikaci odolnou vůči scénářům, kdy uživatelé nezadali data, zůstanou buď používat místní funkce, nebo nastavit výchozí hodnoty v textu lambda, a ne na stručnější navrženou syntaxi.

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);
});

Navrhovaná syntaxe má také výhodu omezení matoucích rozdílů mezi lambdami a místními funkcemi, což usnadňuje zdůvodnění konstruktorů a "zvětšování" lambda na funkce bez ohrožení funkcí, zejména v jiných scénářích, kdy se lambda používají v rozhraních API, kde je možné skupiny metod poskytovat také jako odkazy. To je také hlavní motivace pro podporu params pole, které není pokryto výše uvedeným scénářem použití.

Například:

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

Předchozí chování

Před C# 12, když uživatel implementuje lambda s volitelným nebo params parametrem, kompilátor vyvolá chybu.

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

Když se uživatel pokusí použít skupinu metod, ve které má podkladová metoda volitelný nebo params parametr, tyto informace se nerozšíří, takže volání metody nekontroluje typ kvůli neshodě v počtu očekávaných 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[]>'

Nové chování

Po provedení tohoto návrhu (část jazyka C# 12) lze výchozí hodnoty a params použít u parametrů lambda s následujícím chováním:

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

Výchozí hodnoty a params lze použít u parametrů skupiny metod tím, že konkrétně definujete tuto skupinu metod:

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

Změna narušující kompatibilitu

Před C# 12 je odvozený typ skupiny metod Action nebo Func, takže se zkompiluje následující kód:

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 });
}

Po provedení této změny (část jazyka C# 12) přestane kód této povahy kompilovat v sadě .NET SDK 7.0.200 nebo novější.

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 });
}

Je potřeba zvážit dopad této zásadní změny. Použití var k odvození typu skupiny metod bylo naštěstí podporováno pouze od verze C# 10, takže pouze kód, který byl napsán od té doby, který explicitně spoléhá na toto chování, by se přerušil.

Podrobný návrh

Změny gramatiky a analyzátoru

Toto vylepšení vyžaduje následující změny gramatiky pro výrazy lambda.

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

Všimněte si, že to umožňuje výchozí hodnoty parametrů a pole params pouze pro lambda, ne pro anonymní metody deklarované pomocí syntaxe delegate { }.

Stejná pravidla jako parametry metody (§15.6.2) platí pro parametry lambda:

  • Parametr s modifikátorem ref, out nebo this nemůže mít default_argument.
  • K parameter_array může dojít po volitelném parametru, ale nemůže mít výchozí hodnotu – vynechání argumentů pro parameter_array by místo toho vedlo k vytvoření prázdného pole.

Pro skupiny metod nejsou nutné žádné změny gramatiky, protože tento návrh by změnil pouze jejich sémantiku.

Pro převody anonymních funkcí (§10.7) je nutné přidání následujícího textu (zvýrazněného tučně):

Anonymní funkce F je konkrétně kompatibilní s typem delegáta D:

  • [...]
  • Pokud má F explicitně zadaný seznam parametrů, každý parametr v D má stejný typ a modifikátory jako odpovídající parametr v Fignoruje modifikátory params a výchozí hodnoty.

Aktualizace předchozích návrhů

Do specifikace typů funkcí v předchozím návrhu se vyžaduje následující doplnění (v tučně):

Skupina metod má přirozený typ, pokud všechny kandidátské metody ve skupině metod mají společný podpis včetně výchozích hodnot a modifikátorů params. (Pokud skupina metod může zahrnovat rozšiřující metody, kandidáti zahrnují obsahující typ a všechny obory metod rozšíření.)

Přirozený typ anonymního výrazu funkce nebo skupiny metod je function_type. function_type představuje podpis metody: typy parametrů, výchozí hodnoty, typy ref, params modifikátorya návratový typ a druh odkazu. Anonymní výrazy funkcí nebo skupiny metod se stejným podpisem mají stejný function_type.

Následující dodatek (tučným písmem) je vyžadován ke specifikaci typů delegátů v předchozím návrhu:

Typ delegáta pro anonymní funkci nebo skupinu metod s typy parametrů P1, ..., Pn a návratovým typem R je:

  • pokud některý parametr nebo návratová hodnota není podle hodnoty, nebo jakýkoli parametr je nepovinný nebo params, nebo existuje více než 16 parametrů nebo některý z typů parametrů nebo návrat nejsou platné argumenty typu (například (int* p) => { }), pak delegát je syntetizovaný internal anonymní typ delegáta s podpisem, který odpovídá anonymní funkci nebo skupině metod, a s názvy parametrů arg1, ..., argn nebo arg, pokud jeden parametr; [...]

Změny pořadače

Synchronizace nových typů delegátů

Stejně jako chování delegátů s parametry ref nebo out jsou typy delegátů syntetizovány pro lambda nebo skupiny metod definované s volitelnými nebo params parametry. Všimněte si, že v následujících příkladech se k reprezentaci těchto anonymních typů delegátů používá notace a', b'atd.

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 = " ");

Chování při převodu a sjednocení

Anonymní delegáti s volitelnými parametry budou sjednoceni, pokud má stejný parametr (na základě pozice) stejnou výchozí hodnotu bez ohledu na název parametru.

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

Anonymní delegáti s polem jako posledním parametrem budou sjednoceni, pokud má poslední parametr stejný modifikátor params a typ pole bez ohledu na název parametru.

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)`

Podobně je samozřejmě kompatibilita s pojmenovanými delegáty, kteří již podporují volitelné a params parametry. Pokud se výchozí hodnoty nebo modifikátory params liší v převodu, zdrojová hodnota nebude použita, pokud je ve výrazu lambda, protože lambda nelze volat žádným jiným způsobem. To může uživatelům připadat jako neintuitivní, proto se vygeneruje upozornění, když je k dispozici výchozí hodnota zdroje nebo modifikátor params a liší se od cílové hodnoty. Pokud je zdrojem skupina metod, může být volána samostatně, a proto se nevygeneruje žádné upozornění.

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

Chování IL/běhové prostředí

Výchozí hodnoty parametrů budou generovány do metadat. IL pro tuto funkci bude velmi podobné tomu, jaké IL je generované pro lambdu s parametry ref a out. Třída, která dědí z System.Delegate nebo podobné, se vygeneruje a metoda Invoke bude obsahovat direktivy .param k nastavení výchozích hodnot parametrů nebo System.ParamArrayAttribute – stejně jako tomu je u standardního pojmenovaného delegáta s volitelnými nebo params parametry.

Tyto typy delegátů je možné kontrolovat za běhu jako obvykle. V kódu mohou uživatelé introspektovat DefaultValue v ParameterInfo přidružené ke skupině lambda nebo metody pomocí přidružené 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

Otevřené otázky

Žádná z těchto možností nebyla implementována. Zůstávají otevřenými návrhy.

otevřená otázka: jak se to interaguje s existujícím atributem DefaultParameterValue?

Navrhovaná odpověď: Pro paritu povolte atribut DefaultParameterValue pro lambda a ujistěte se, že chování při generování delegáta odpovídá výchozím hodnotám parametrů podporovaným prostřednictvím syntaxe.

var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed

otevřená otázka: Nejprve mějte na paměti, že toto je mimo rozsah aktuálního návrhu, ale v budoucnu by mohlo být vhodné probrat. Chceme podporovat výchozí hodnoty s implicitně zadanými parametry lambda? Tj.

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

Toto odvození vede k některým složitým problémům s převodem, které by vyžadovaly další diskuzi.

Zde jsou také úvahy o výkonu při parsování. Dnes by například termín (x = nikdy nebyl začátek výrazu lambda. Pokud by tato syntaxe byla povolena pro výchozí hodnoty lambda, analyzátor by potřeboval delší pohled dopředu (aby prohledal až k tokenu =>), aby určil, zda je výraz lambda či nikoliv.

Schůzky o návrhu

  • LDM 2022-10-10: rozhodnutí přidat podporu pro params stejným způsobem jako výchozí hodnoty parametrů.