Opcjonalne parametry i parametry tablicowe dla wyrażeń lambda i grup metod
Notatka
Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.
Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).
Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .
Streszczenie
Aby bazować na ulepszeniach lambda wprowadzonych w C# 10 (zobacz istotne tło), proponujemy dodanie obsługi domyślnych wartości parametrów i tablic params
w wyrażeniach lambda. Umożliwiłoby to użytkownikom zaimplementowanie następujących wyrażeń 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
Podobnie zezwolimy na takie samo zachowanie grup 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;
}
Odpowiednie tło
Ulepszenia "lambda" w języku C# 10
Specyfikacja konwersji grup metod §10.8
Motywacja
Struktury aplikacji w ekosystemie platformy .NET wykorzystują w dużym stopniu lambdy, aby umożliwić użytkownikom szybkie pisanie logiki biznesowej skojarzonej z punktem końcowym.
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);
});
Lambdy nie obsługują obecnie ustawiania wartości domyślnych dla parametrów, więc jeśli deweloper chce stworzyć aplikację odporną na sytuacje, w których użytkownicy nie dostarczają danych, musi skorzystać albo z funkcji lokalnych, albo ustawić wartości domyślne w samej lambdzie, co jest mniej zwięzłe niż proponowana składnia.
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);
});
Przewagą proponowanej składni jest również zmniejszenie mylących różnic między funkcjami lambda a lokalnymi, co ułatwia przewidywanie zachowania konstrukcji oraz przekształcanie lambd w funkcje bez ograniczania ich możliwości, szczególnie w innych scenariuszach, w których lambdy są używane w interfejsach API, gdzie grupy metod mogą być również podawane jako referencje.
Jest to również główna motywacja do poparcia tablicy params
, która nie jest objęta wspomnianym wcześniej scenariuszem użycia.
Na przykład:
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);
Poprzednie zachowanie
Przed C# 12, gdy użytkownik implementuje wyrażenie lambda z opcjonalnym parametrem lub parametrem params
, kompilator sygnalizuje błąd.
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
Gdy użytkownik próbuje użyć grupy metod, w której podstawowa metoda ma opcjonalny lub params
parametr, te informacje nie są przekazywane, więc wywołanie metody nie przechodzi sprawdzania typu z powodu rozbieżności w liczbie oczekiwanych argumentów.
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[]>'
Nowe zachowanie
Po wykonaniu tej propozycji (część języka C# 12) wartości domyślne i params
można zastosować do parametrów lambda przy użyciu następującego zachowania:
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
Wartości domyślne i params
można zastosować do parametrów grupy metod, definiując w szczególności taką grupę 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
Zmiana powodująca niezgodność
Przed C# 12 wywnioskowany typ grupy metod jest Action
lub Func
, więc następujący kod się kompiluje:
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 tej zmianie (część języka C# 12) kod tego rodzaju przestaje być kompilowany w zestawie .NET SDK 7.0.200 lub nowszym.
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 });
}
Należy wziąć pod uwagę wpływ tej zmiany powodującej niezgodność. Na szczęście użycie var
do wnioskowania typu grupy metod było obsługiwane dopiero od C# 10, więc tylko kod, napisany od tego czasu, który jawnie polega na tym zachowaniu, może powodować błędy.
Szczegółowy projekt
Zmiany gramatyki i analizatora
To ulepszenie wymaga następujących zmian gramatyki w wyrażeniach 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?
;
Pamiętaj, że domyślne wartości parametrów i tablice params
są obsługiwane tylko dla lambd, a nie dla metod anonimowych w składni delegate { }
.
Te same reguły co w przypadku parametrów metody (§15.6.2) mają zastosowanie do parametrów lambda:
- Parametr z modyfikatorem
ref
,out
lubthis
nie może mieć default_argument. - parameter_array może wystąpić po opcjonalnym parametrze, ale nie może mieć wartości domyślnej — pominięcie argumentów dla parameter_array spowodowałoby utworzenie pustej tablicy.
W przypadku grup metod nie są konieczne żadne zmiany gramatyki, ponieważ ta propozycja zmieniłaby tylko ich semantyka.
Następujące dodanie (wytłuszczenie) jest wymagane dla konwersji anonimowych funkcji (§10.7):
W szczególności funkcja anonimowa
F
jest zgodna z typem delegataD
, jeżeli:
- [...]
- Jeśli
F
ma jawnie zdefiniowaną listę typów parametrów, każdy parametr wD
ma ten sam typ i modyfikatory co odpowiedni parametr wF
, z wyjątkiem modyfikatorówparams
i wartości domyślnych.
Aktualizacje wcześniejszych propozycji
Następujący dodatek (wytłuszczony) jest wymagany do specyfikacji typów funkcji w poprzednim wniosku:
Grupa metod ma typ naturalny, jeśli wszystkie metody kandydujące w grupie metod mają wspólną sygnaturę , obejmującą wartości domyślne i modyfikatory
params
. (Jeśli grupa metod może zawierać metody rozszerzenia, kandydaci obejmują typ zawierający i wszystkie zakresy metody rozszerzenia).
Naturalnym typem anonimowego wyrażenia funkcji lub grupy metod jest function_type. function_type reprezentuje sygnaturę metody: typy parametrów, wartości domyślne, rodzaje ref, modyfikatory
params
oraz typ zwracany i rodzaj odwołania. Anonimowe wyrażenia funkcji lub grupy metod z tą samą sygnaturą mają ten sam function_type.
Następujący dodatek (w pogrubieniu) jest konieczny w specyfikacji typów delegatów w poprzedniej propozycji:
Typ delegata dla anonimowej funkcji lub grupy metod z typami parametrów
P1, ..., Pn
i zwracany typR
to:
- Jeśli dowolny parametr lub wartość zwracana nie jest poprzez wartość, lub dowolny parametr jest opcjonalny lub
params
, lub istnieje więcej niż 16 parametrów, lub czy którykolwiek z typów parametrów lub typ wartości zwracanej jest nieprawidłowym argumentem typu (np.(int* p) => { }
), to delegat jest syntetyzowany jakointernal
anonimowy typ delegata z podpisem zgodnym z anonimową funkcją lub grupą metod oraz z nazwami parametrówarg1, ..., argn
lubarg
, jeśli jest to pojedynczy parametr; [...]
Zmiany bindera
Syntezowanie nowych typów delegatów
Podobnie jak w przypadku zachowania delegatów z parametrami ref
lub out
, typy delegatów są syntetyzowane dla wyrażeń lambda lub grup metodowych zdefiniowanych przy użyciu parametrów opcjonalnych lub params
.
Należy pamiętać, że w poniższych przykładach notacja a'
, b'
itp. służy do reprezentowania tych anonimowych typów delegatów.
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 = " ");
Zachowanie konwersji i ujednolicenia
Anonimowe delegaty z opcjonalnymi parametrami będą ujednolicone, gdy ten sam parametr (na podstawie pozycji) ma tę samą wartość domyślną, niezależnie od nazwy 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
Anonimowe delegaty z tablicą jako ostatnim parametrem będą ujednolicone, gdy ostatni parametr ma taki sam modyfikator params
i typ tablicy, niezależnie od nazwy 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)`
Podobnie istnieje oczywiście zgodność z nazwanymi delegatami, które już obsługują parametry opcjonalne i params
.
Jeśli wartości domyślne lub modyfikatory params
różnią się podczas konwersji, te zdefiniowane w źródle nie będą używane, jeśli znajdują się w wyrażeniu lambda, ponieważ lambdy nie można wywołać w inny sposób.
Może to wydawać się sprzeczne z intuicją dla użytkowników, dlatego ostrzeżenie będzie emitowane, gdy wartość domyślna źródła lub params
modyfikator jest obecny i różni się od docelowego.
Jeśli źródło jest grupą metod, można ją wywołać samodzielnie, dlatego żadne ostrzeżenie nie zostanie wyemitowane.
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
Zachowanie IL/środowiska uruchomieniowego
Domyślne wartości parametrów będą emitowane do metadanych. Il dla tej funkcji będzie bardzo podobny do IL emitowanego dla lambd z parametrami ref
i out
. Zostanie wygenerowana klasa dziedzicząca z System.Delegate
lub podobnej, a metoda Invoke
będzie zawierać dyrektywy .param
do ustawienia domyślnych wartości parametrów lub System.ParamArrayAttribute
— podobnie jak w przypadku standardowego nazwanego delegata z opcjonalnymi lub params
parametrami.
Te typy delegatów można sprawdzić w czasie wykonywania, jak zwykle.
W kodzie użytkownicy mogą analizować DefaultValue
w ParameterInfo
powiązanej z grupą lambd lub metod, korzystając z powiązanego 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
Otwórz pytania
Żadna z nich nie została wdrożona. Pozostają otwarte propozycje.
Pytanie otwarte: w jaki sposób ta funkcja współpracuje z istniejącym atrybutem DefaultParameterValue
?
Proponowana odpowiedź: Dla zgodności, zezwól na użycie atrybutu DefaultParameterValue
w wyrażeniach lambda i upewnij się, że działanie generacji delegatów jest zgodne z obsługiwanymi wartościami domyślnych parametrów poprzez składnię.
var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed
Otwarte pytanie: Po pierwsze, należy pamiętać, że wykracza to poza zakres obecnej propozycji, ale warto omawiać to w przyszłości. Czy chcemy obsługiwać wartości domyślne z niejawnie wpisanymi parametrami 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
To wnioskowanie prowadzi do niektórych trudnych problemów z konwersją, które wymagają większej dyskusji.
Poniżej przedstawiono również zagadnienia dotyczące analizowania wydajności. Na przykład dzisiaj termin (x =
nigdy nie może być początkiem wyrażenia lambda. Jeśli taka składnia byłaby dozwolona dla domyślnych wartości lambda, analizator składni potrzebowałby bardziej zaawansowanego spojrzenia wprzód (przeskanowania aż do tokenu =>
), aby ustalić, czy dany element jest lambdą, czy nie.
Spotkania projektowe
-
LDM 2022-10-10: decyzja o dodaniu obsługi
params
w taki sam sposób, jak wartości domyślne parametrów.
C# feature specifications