Udostępnij za pośrednictwem


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:

  1. Zezwalaj na wyrażenia lambda z atrybutami
  2. Zezwalaj na wyrażenia lambda z jawnym typem zwrotnym
  3. 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 (§12.19) (wyrażenie lambda lub metoda anonimowa ) ma typ naturalny, jeśli typy parametrów są jawne, a zwracany typ jest jawny lub może zostać wywnioskowany (zobacz §12.6.3.13).

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_typeG, jeśli parametry i zwracane typy F są konwertowalne względem wariancji do parametrów i zwracanych typów G
  • Aby System.MulticastDelegate albo klasy bazowe oraz interfejsy System.MulticastDelegate
  • Aby System.Linq.Expressions.Expression lub System.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ą , a Ti jest typem delegata lub typem drzewa wyrażeń, jawne wnioskowanie typu parametru jest wykonywane z Ei do Ti, a jawne wnioskowanie typu zwracanego jest wykonywane z Ei do Ti.
  • W przeciwnym razie, jeśli Ei ma typ U, a xi jest parametrem wartości, wnioskowania niższego jest wykonywane zUdoTi.
  • W przeciwnym razie, jeśli Ei ma typ U, a xi jest parametrem ref lub out, dokładne wnioskowanie jest wykonywane zUdoTi.
  • 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 Edo typu T w następujący sposób:

  • Jeśli E jest funkcją anonimową z jawnym typem zwracanym Ur, a T jest typem delegata lub typem drzewa wyrażeń z typem zwracanym Vr, dokładne wnioskowanie (§12.6.3.9) jest wykonywane zUrdoVr.

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ń dla Xi, 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 granicy U z Xi wszystkie typy Uj, które nie są identyczne z U, są usuwane z zestawu kandydatów. Dla każdej dolnej granicy U z Xi wszystkie typy Uj, do których nie istnieje niejawna konwersja z U, są usuwane z zestawu kandydatów. Dla każdej górnej granicy UXi wszystkich typów Uj, z których nie ma niejawnej konwersji na U są usuwane z zestawu kandydatów.
  • Jeśli wśród pozostałych typów kandydatów Uj istnieje wyjątkowy typ V, dla którego istnieje niejawna konwersja na wszystkie inne typy kandydatów, to Xi jest ustalony na V.
  • 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 jako internal anonimowy typ delegata z sygnaturą pasującą do anonimowej funkcji lub grupy metod oraz z nazwami parametrów arg1, ..., argn lub arg, jeśli jest to pojedynczy parametr.
  • jeśli R jest void, typ delegata jest System.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 lepiej funkcji niż , jeśli

  1. dla każdego argumentu niejawna konwersja z Ex na Px nie jest konwersją typów funkcjii
    • Mp jest metodą niegeneryjną lub Mp jest metodą ogólną z parametrami typu {X1, X2, ..., Xp} i dla każdego parametru typu Xi argument typu jest wnioskowany z wyrażenia lub typu innego niż function_typei
    • dla co najmniej jednego argumentu niejawna konwersja z Ex na Qx jest konwersją_typu_funkcji, lub Mq jest metodą ogólną z parametrami typu {Y1, Y2, ..., Yq}, i dla co najmniej jednego parametru typu Yi argument typu jest wnioskowany z typu_funkcji, lub
  2. dla każdego argumentu niejawna konwersja z Ex na Qx nie jest lepsza niż niejawna konwersja z Ex na Pxi dla co najmniej jednego argumentu konwersja z Ex na Px jest lepsza niż konwersja z Ex na Qx.

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żenia E na typ T1, oraz niejawne C2 konwersje, które konwertują z wyrażenia E na typ T2, C1 jest lepszą konwersją niż C2, jeśli:

  1. C1 nie jest function_type_conversion i C2 jest function_type_conversionlub
  2. E jest niestały interpolated_string_expression, C1 jest implicit_string_handler_conversion, T1 jest applicable_interpolated_string_handler_type, oraz C2 nie jest implicit_string_handler_conversion.
  3. E nie jest dokładnie zgodny z T2 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?