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
nebothis
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átaD
:
- [...]
- Pokud má
F
explicitně zadaný seznam parametrů, každý parametr vD
má stejný typ a modifikátory jako odpovídající parametr vF
ignoruje modifikátoryparams
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 typemR
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
neboarg
, 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
C# feature specifications