Ulepszenia lambda
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
Proponowane zmiany:
- Zezwalaj na wyrażenia lambda z atrybutami
- Zezwalaj na wyrażenia lambda z jawnym typem zwrotnym
- Ustalanie naturalnego typu delegata dla lambd i grup metod
Motywacja
Obsługa atrybutów w wyrażeniach lambda zapewnia równoważność metod i funkcji lokalnych.
Obsługa jawnych typów zwracanych zapewnia symetrię z parametrami lambda, w których można określić jawne typy. Zezwolenie na stosowanie jawnych typów zwracanych dałoby również kontrolę nad wydajnością kompilatora w zagnieżdżonych lambdach, gdzie rozwiązywanie przeciążenia obecnie musi wiązać ciało lambdy, aby określić sygnaturę.
Typ naturalny dla wyrażeń lambda i grup metod umożliwi więcej scenariuszy, w których wyrażenia lambda i grupy metod mogą być używane bez jawnego typu delegata, w tym jako inicjatory w deklaracjach var
.
Wymaganie jawnych typów delegatów dla lambd i grup metod jest punktem konfliktowym dla klientów i stało się utrudnieniem w postępach ASP.NET przy ostatnich pracach nad MapAction.
ASP.NET MapAction bez zmian (MapAction()
przyjmuje argument System.Delegate
):
[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction((Func<Todo>)GetTodo);
[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction((Func<Todo, Todo>)PostTodo);
ASP.NET MapAction z typami naturalnymi dla grup metod:
[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction(GetTodo);
[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction(PostTodo);
ASP.NET MapAction z atrybutami i typami naturalnymi dla wyrażeń lambda:
app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo);
Atrybuty
Atrybuty można dodać do wyrażeń lambda i parametrów lambda. Aby uniknąć niejednoznaczności między atrybutami metody a atrybutami parametrów, wyrażenie lambda z atrybutami musi używać listy parametrów w nawiasach. Typy parametrów nie są wymagane.
f = [A] () => { }; // [A] lambda
f = [return:A] x => x; // syntax error at '=>'
f = [return:A] (x) => x; // [A] lambda
f = [A] static x => x; // syntax error at '=>'
f = ([A] x) => x; // [A] x
f = ([A] ref int x) => x; // [A] x
Można określić wiele atrybutów rozdzielonych przecinkami na tej samej liście atrybutów lub jako oddzielne listy atrybutów.
var f = [A1, A2][A3] () => { }; // ok
var g = ([A1][A2, A3] int x) => x; // ok
Atrybuty nie są obsługiwane w przypadku metod anonimowych zadeklarowanych za pomocą składni delegate { }
.
f = [A] delegate { return 1; }; // syntax error at 'delegate'
f = delegate ([A] int x) { return x; }; // syntax error at '['
Analizator będzie przeglądać kod, aby odróżnić inicjator kolekcji z przypisaniem elementu od inicjatora kolekcji z wyrażeniem lambda.
var y = new C { [A] = x }; // ok: y[A] = x
var z = new C { [A] x => x }; // ok: z[0] = [A] x => x
Analizator będzie traktować ?[
jako początek dostępu do elementu warunkowego.
x = b ? [A]; // ok
y = b ? [A] () => { } : z; // syntax error at '('
Atrybuty w wyrażeniu lambda lub parametrach lambda będą emitowane do metadanych w metodzie mapowanej na lambda.
Ogólnie rzecz biorąc, klienci nie powinni zależeć od sposobu mapowania wyrażeń lambda i funkcji lokalnych ze źródła na metadane. Sposób emitowania funkcji lambda i funkcji lokalnych może i zmienia się między wersjami kompilatora.
Proponowane tutaj zmiany dotyczą scenariusza opartego na Delegate
.
Powinno być możliwe sprawdzenie MethodInfo
skojarzonego z wystąpieniem Delegate
, aby określić podpis wyrażenia lambda lub funkcji lokalnej, w tym atrybutów jawnych i dodatkowych metadanych generowanych przez kompilator, takich jak parametry domyślne.
Dzięki temu zespoły, takie jak ASP.NET, udostępniają te same zachowania dla lambd i funkcji lokalnych, co zwykłe metody.
Wyraźny typ zwracany
Przed ujętą w nawiasy listą parametrów można określić jawny typ zwracany.
f = T () => default; // ok
f = short x => 1; // syntax error at '=>'
f = ref int (ref int x) => ref x; // ok
f = static void (_) => { }; // ok
f = async async (async async) => async; // ok?
Analizator przyjrzy się temu, aby odróżnić wywołanie metody T()
od wyrażenia lambda T () => e
.
Jawne typy zwracane nie są obsługiwane w przypadku metod anonimowych zadeklarowanych przy użyciu składni delegate { }
.
f = delegate int { return 1; }; // syntax error
f = delegate int (int x) { return x; }; // syntax error
Wnioskowanie typu metody powinno spowodować dokładne wnioskowanie z jawnego typu zwracanego lambda.
static void F<T>(Func<T, T> f) { ... }
F(int (i) => i); // Func<int, int>
Konwersje wariancji nie są dozwolone z typu zwracanego przez funkcję lambda do typu zwracanego przez delegata (co jest zgodne z podobnym zachowaniem dla typów parametrów).
Func<object> f1 = string () => null; // error
Func<object?> f2 = object () => x; // warning
Analizator umożliwia użycie wyrażeń lambda z typami zwracanymi ref
w wyrażeniach bez dodatkowych nawiasów.
d = ref int () => x; // d = (ref int () => x)
F(ref int () => x); // F((ref int () => x))
var
nie może być używane jako jawny typ zwrotny dla wyrażeń lambda.
class var { }
d = var (var v) => v; // error: contextual keyword 'var' cannot be used as explicit lambda return type
d = @var (var v) => v; // ok
d = ref var (ref var v) => ref v; // error: contextual keyword 'var' cannot be used as explicit lambda return type
d = ref @var (ref var v) => ref v; // ok
Typ naturalny (funkcja)
Wyrażenie funkcji anonimowej
Grupa metod ma typ naturalny, o ile wszystkie metody kandydujące w tej grupie mają wspólny podpis. (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 i rodzaje ref oraz typ zwracany i rodzaj ref. Anonimowe wyrażenia funkcji lub grupy metod z tą samą sygnaturą mają te same function_type.
Function_types są używane tylko w kilku określonych kontekstach:
- niejawne i jawne konwersje
- wnioskowanie typu dla metod (§12.6.3) i najlepszy wspólny typ (§12.6.3.15)
- inicjatory
var
Wyłącznie w czasie kompilacji istnieje function_type: function_types nie pojawiają się w kodzie źródłowym ani w metadanych.
Konwersje
Z function_typeF
wynikają niejawne konwersje function_type:
- Do function_type
G
, jeśli parametry i zwracane typyF
są konwertowalne względem wariancji do parametrów i zwracanych typówG
- Aby
System.MulticastDelegate
albo klasy bazowe oraz interfejsySystem.MulticastDelegate
- Aby
System.Linq.Expressions.Expression
lubSystem.Linq.Expressions.LambdaExpression
Anonimowe wyrażenia funkcji i grupy metod mają już konwersje z wyrażenia do delegowania typów i typów drzewa wyrażeń (zobacz anonimowe konwersje funkcji §10.7 i konwersje grup metod §10.8). Te konwersje są wystarczające do konwersji na silnie typizowane typy delegatów i typy drzewa wyrażeń. Powyższe konwersje typu funkcji dodają konwersje z typu wyłącznie do typów podstawowych: System.MulticastDelegate
, System.Linq.Expressions.Expression
, itp.
Nie ma konwersji na function_type z typu innego niż function_type. Nie ma jawnych konwersji dla function_types, ponieważ nie można odwoływać się do function_types w źródle.
Konwersja na System.MulticastDelegate
, typ podstawowy lub interfejs, realizuje anonimową funkcję lub grupę metod jako instancję odpowiedniego typu delegata.
Konwersja na typ System.Linq.Expressions.Expression<TDelegate>
lub typ bazowy realizuje wyrażenie lambda jako drzewo wyrażeń z odpowiednim typem delegata.
Delegate d = delegate (object obj) { }; // Action<object>
Expression e = () => ""; // Expression<Func<string>>
object o = "".Clone; // Func<object>
Function_type konwersje nie są konwersjami standardowymi, ani jawnymi, ani niejawnymi §10.4 i nie są brane pod uwagę podczas określania tego, czy operator konwersji zdefiniowanej przez użytkownika ma aplikacje do anonimowej funkcji lub grupy metod. Na podstawie oceny konwersji zdefiniowanych przez użytkownika §10.5.3:
Aby operator konwersji mógł mieć zastosowanie, musi być możliwe przeprowadzenie konwersji standardowej (§10.4) z typu źródłowego do typu operand operatora i musi być możliwe przeprowadzenie standardowej konwersji z typu wyniku operatora na typ docelowy.
class C
{
public static implicit operator C(Delegate d) { ... }
}
C c;
c = () => 1; // error: cannot convert lambda expression to type 'C'
c = (C)(() => 2); // error: cannot convert lambda expression to type 'C'
Ostrzeżenie jest zgłaszane dla niejawnej konwersji grupy metod na object
, ponieważ konwersja jest prawidłowa, ale być może niezamierzona.
Random r = new Random();
object obj;
obj = r.NextDouble; // warning: Converting method group to 'object'. Did you intend to invoke the method?
obj = (object)r.NextDouble; // ok
Wnioskowanie typów
Istniejące reguły wnioskowania typu są w większości niezmienione (zobacz §12.6.3). Istnieje jednak kilka zmian poniżej do określonych faz wnioskowania typu.
Pierwsza faza
Pierwsza faza (§12.6.3.2) zezwala funkcji anonimowej na powiązanie się z Ti
, nawet jeśli Ti
nie jest typem delegata lub drzewem wyrażeń (być może parametrem typu ograniczonym na przykład do System.Delegate
).
Dla każdego argumentu metody
Ei
:
- Jeśli
Ei
jest funkcją anonimową , aTi
jest typem delegata lub typem drzewa wyrażeń, jawne wnioskowanie typu parametru jest wykonywane zEi
doTi
, a jawne wnioskowanie typu zwracanego jest wykonywane zEi
doTi
.- W przeciwnym razie, jeśli
Ei
ma typU
, axi
jest parametrem wartości, wnioskowania niższego jest wykonywane zU
doTi
.- W przeciwnym razie, jeśli
Ei
ma typU
, axi
jest parametremref
lubout
, dokładne wnioskowanie jest wykonywane zU
doTi
.- W przeciwnym razie dla tego argumentu nie jest wykonywane żadne wnioskowanie.
jawne wnioskowanie typu zwracanego
jawne określenie typu zwrotnego jest wykonywane na podstawie wyrażenia
E
do typuT
w następujący sposób:
- Jeśli
E
jest funkcją anonimową z jawnym typem zwracanymUr
, aT
jest typem delegata lub typem drzewa wyrażeń z typem zwracanymVr
, dokładne wnioskowanie (§12.6.3.9) jest wykonywane zUr
doVr
.
Naprawa
Poprawianie (§12.6.3.12) zapewnia, że inne konwersje są preferowane nad konwersjami typu function_type. (Wyrażenia lambda i wyrażenia grupy metod przyczyniają się tylko do niższych granic, więc obsługa function_types jest wymagana tylko w przypadku niższych granic).
Niefiksowana zmienna typu
z zestawem granic jest stała w następujący sposób:
- Zestaw typów kandydatów
Uj
początkowo stanowi zestaw wszystkich typów w zestawie ograniczeń dlaXi
, gdzie typy funkcji są pomijane w niższych granicach, jeśli istnieją jakiekolwiek typy, które nie są typami funkcji.- Następnie badamy każdą granicę dla
Xi
po kolei: dla każdej dokładnej granicyU
zXi
wszystkie typyUj
, które nie są identyczne zU
, są usuwane z zestawu kandydatów. Dla każdej dolnej granicyU
zXi
wszystkie typyUj
, do których nie istnieje niejawna konwersja zU
, są usuwane z zestawu kandydatów. Dla każdej górnej granicyU
Xi
wszystkich typówUj
, z których nie ma niejawnej konwersji naU
są usuwane z zestawu kandydatów.- Jeśli wśród pozostałych typów kandydatów
Uj
istnieje wyjątkowy typV
, dla którego istnieje niejawna konwersja na wszystkie inne typy kandydatów, toXi
jest ustalony naV
.- W przeciwnym razie wnioskowanie typu kończy się niepowodzeniem.
Najlepszy typ powszechny
Najlepszy typ typowy (§12.6.3.15) jest definiowany pod względem wnioskowania typu, więc powyższe zmiany wnioskowania typu mają zastosowanie również do najlepszego typu.
var fs = new[] { (string s) => s.Length, (string s) => int.Parse(s) }; // Func<string, int>[]
var
Funkcje anonimowe i grupy metod z typami funkcji mogą być używane jako inicjatory w deklaracjach var
.
var f1 = () => default; // error: cannot infer type
var f2 = x => x; // error: cannot infer type
var f3 = () => 1; // System.Func<int>
var f4 = string () => null; // System.Func<string>
var f5 = delegate (object o) { }; // System.Action<object>
static void F1() { }
static void F1<T>(this T t) { }
static void F2(this string s) { }
var f6 = F1; // error: multiple methods
var f7 = "".F1; // error: the delegate type could not be inferred
var f8 = F2; // System.Action<string>
Typy funkcji nie są używane w przypisaniach do wywołań typu discard.
d = () => 0; // ok
_ = () => 1; // error
Typy delegatów
Typ delegata dla anonimowej funkcji lub grupy metod z parametrami typu P1, ..., Pn
i typem zwrotu R
to:
- Jeśli jakikolwiek parametr lub wartość zwracana nie jest przekazywana przez wartość, lub istnieje więcej niż 16 parametrów, lub którykolwiek z typów parametrów lub wartość zwracana są nieprawidłowymi argumentami typu (np.
(int* p) => { }
), delegat jest syntetyzowany jakointernal
anonimowy typ delegata z sygnaturą pasującą do anonimowej funkcji lub grupy metod oraz z nazwami parametrówarg1, ..., argn
lubarg
, jeśli jest to pojedynczy parametr. - jeśli
R
jestvoid
, typ delegata jestSystem.Action<P1, ..., Pn>
; - W przeciwnym wypadku typ delegata to
System.Func<P1, ..., Pn, R>
.
Kompilator może w przyszłości zezwolić na powiązanie większej liczby podpisów z typami System.Action<>
i System.Func<>
(na przykład jeśli typy ref struct
są dozwolone jako argumenty typu).
modopt()
lub modreq()
w sygnaturze grupy metod są ignorowane w odpowiednim typie delegata.
Jeśli dwie anonimowe funkcje lub grupy metod w tej samej kompilacji wymagają syntetyzowanych typów delegatów z tymi samymi typami parametrów i modyfikatorami, kompilator użyje tego samego syntetyzowanego typu delegata.
Rozpoznawanie przeciążenia
Element członkowski funkcji Better (§12.6.4.3) jest aktualizowany w celu preferowania elementów członkowskich, w których żadna z konwersji i żaden z argumentów typu nie zawiera wywnioskowanych typów z wyrażeń lambda lub grup metod.
Lepszy członek funkcji
... Biorąc pod uwagę listę argumentów
z zestawem wyrażeń argumentów i dwoma odpowiednimi elementami członkowskimi funkcji i z typami parametrów i , jest definiowana jako funkcji niż lepiej , jeśli
- dla każdego argumentu niejawna konwersja z
Ex
naPx
nie jest konwersją typów funkcjii
Mp
jest metodą niegeneryjną lubMp
jest metodą ogólną z parametrami typu{X1, X2, ..., Xp}
i dla każdego parametru typuXi
argument typu jest wnioskowany z wyrażenia lub typu innego niż function_typei- dla co najmniej jednego argumentu niejawna konwersja z
Ex
naQx
jest konwersją_typu_funkcji, lubMq
jest metodą ogólną z parametrami typu{Y1, Y2, ..., Yq}
, i dla co najmniej jednego parametru typuYi
argument typu jest wnioskowany z typu_funkcji, lub- dla każdego argumentu niejawna konwersja z
Ex
naQx
nie jest lepsza niż niejawna konwersja zEx
naPx
i dla co najmniej jednego argumentu konwersja zEx
naPx
jest lepsza niż konwersja zEx
naQx
.
Lepsza konwersja z wyrażenia (§12.6.4.5) jest aktualizowana w celu preferowania konwersji, które nie obejmowały wywnioskowanych typów z wyrażeń lambda lub grup metod.
Ulepszona konwersja wyrażeń
Biorąc pod uwagę niejawne
C1
konwersje, które konwertują z wyrażeniaE
na typT1
, oraz niejawneC2
konwersje, które konwertują z wyrażeniaE
na typT2
,C1
jest lepszą konwersją niżC2
, jeśli:
C1
nie jest function_type_conversion iC2
jest function_type_conversionlubE
jest niestały interpolated_string_expression,C1
jest implicit_string_handler_conversion,T1
jest applicable_interpolated_string_handler_type, orazC2
nie jest implicit_string_handler_conversion.E
nie jest dokładnie zgodny zT2
i co najmniej jeden z następujących warunków jest spełniony:
Składnia
lambda_expression
: modifier* identifier '=>' (block | expression)
| attribute_list* modifier* type? lambda_parameters '=>' (block | expression)
;
lambda_parameters
: lambda_parameter
| '(' (lambda_parameter (',' lambda_parameter)*)? ')'
;
lambda_parameter
: identifier
| attribute_list* modifier* type? identifier equals_value_clause?
;
Otwarte problemy
Czy wartości domyślne powinny być obsługiwane dla parametrów wyrażenia lambda dla kompletności?
Czy System.Diagnostics.ConditionalAttribute
powinno być niedozwolone w wyrażeniach lambda, skoro istnieje niewiele scenariuszy, gdzie można z nich skorzystać warunkowo?
([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?
Czy function_type powinien być dostępny za pośrednictwem interfejsu API kompilatora, oprócz wynikowego typu delegata?
Obecnie wnioskowany typ delegata używa System.Action<>
lub System.Func<>
, gdy typy parametrów i zwracane typy są prawidłowymi argumentami typów i, nie ma więcej niż 16 parametrów, a jeśli oczekiwany typ Action<>
lub Func<>
jest brakujący, zgłaszany jest błąd. Zamiast tego kompilator powinien używać System.Action<>
lub System.Func<>
niezależnie od arity? A jeśli brakuje oczekiwanego typu, zsyntetyzuje typ delegata w przeciwnym razie?
C# feature specifications