Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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 .
Problem z czempionem: https://github.com/dotnet/csharplang/issues/6051
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,outlubthisnie 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
Fjest zgodna z typem delegataD, jeżeli:
- [...]
- Jeśli
Fma jawnie zdefiniowaną listę typów parametrów, każdy parametr wDma ten sam typ i modyfikatory co odpowiedni parametr wF, z wyjątkiem modyfikatorówparamsi 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
paramsoraz 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, ..., Pni zwracany typRto:
- 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 jakointernalanonimowy typ delegata z podpisem zgodnym z anonimową funkcją lub grupą metod oraz z nazwami parametrówarg1, ..., argnlubarg, 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
paramsw taki sam sposób, jak wartości domyślne parametrów.
C# feature specifications