Freigeben über


Verbesserungen bei Lambda

Anmerkung

Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.

Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.

Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Champion Issue: https://github.com/dotnet/csharplang/issues/4934

Zusammenfassung

Vorgeschlagene Änderungen:

  1. Zulassen von Lambdas mit Attributen
  2. Zulassen von Lambdas mit explizitem Rückgabetyp
  3. Ermitteln eines natürlichen Delegattyps für Lambdas und Methodengruppen

Motivation

Die Unterstützung von Attributen für Lambdas würde Parität mit Methoden und lokalen Funktionen bieten.

Die Unterstützung für explizite Rückgabetypen würde die Symmetrie mit Lambda-Parametern bereitstellen, bei denen explizite Typen angegeben werden können. Durch Zulassen expliziter Rückgabetypen kann auch die Compilerleistung in geschachtelten Lambda-Ausdrücken überwacht werden, bei denen die Überladungsauflösung derzeit den Lambdatext binden muss, um die Signatur zu bestimmen.

Ein natürlicher Typ für Lambda-Ausdrücke und Methodengruppen ermöglicht mehr Szenarien, in denen Lambdas und Methodengruppen ohne expliziten Delegattyp verwendet werden können, einschließlich als Initialisierer in var Deklarationen.

Die Anforderung expliziter Delegattypen für Lambdas und Methodengruppen war ein Reibungspunkt für Kunden und wurde zu einem Hindernis für den Fortschritt in ASP.NET durch die jüngste Arbeit an MapAction.

ASP.NET MapAction ohne vorgeschlagene Änderungen (MapAction() akzeptiert ein System.Delegate Argument):

[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 mit natürlichen Typen für Methodengruppen:

[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction(GetTodo);

[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction(PostTodo);

ASP.NET MapAction mit Attributen und natürlichen Typen für Lambda-Ausdrücke:

app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo);

Attribute

Attribute können Lambda-Ausdrücken und Lambda-Parametern hinzugefügt werden. Um Mehrdeutigkeit zwischen Methodenattributen und Parameterattributen zu vermeiden, muss ein Lambda-Ausdruck mit Attributen eine klammerte Parameterliste verwenden. Parametertypen sind nicht erforderlich.

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

Es können mehrere Attribute angegeben werden, entweder durch Kommas getrennt innerhalb derselben Attributliste oder als separate Attributlisten.

var f = [A1, A2][A3] () => { };    // ok
var g = ([A1][A2, A3] int x) => x; // ok

Für anonyme Methoden, die mit der delegate { }-Syntax deklariert werden, werden keine Attribute unterstützt.

f = [A] delegate { return 1; };         // syntax error at 'delegate'
f = delegate ([A] int x) { return x; }; // syntax error at '['

Der Parser sieht voraus, um einen Sammlungsinitialisierer mit einer Elementzuweisung von einem Sammlungsinitialisierer mit einem Lambda-Ausdruck zu unterscheiden.

var y = new C { [A] = x };    // ok: y[A] = x
var z = new C { [A] x => x }; // ok: z[0] = [A] x => x

Der Parser behandelt ?[ als Start eines bedingten Elementzugriffs.

x = b ? [A];               // ok
y = b ? [A] () => { } : z; // syntax error at '('

Die Attribute für Lambda-Ausdrücke oder Lambda-Parameter werden in die Metadaten der Methode aufgenommen, die der Lambda-Funktion zugeordnet ist.

Im Allgemeinen sollten Kunden nicht darauf vertrauen, wie Lambda-Ausdrücke und lokale Funktionen von der Quelle in Metadaten abgebildet werden. Wie Lambdas und lokale Funktionen ausgegeben werden, kann und hat sich zwischen Compilerversionen geändert.

Die hier vorgeschlagenen Änderungen beziehen sich auf das Delegate-orientierte Szenario. Die mit einer MethodInfo-Instanz verknüpfte Delegate sowie die expliziten Attribute und zusätzlichen Metadaten wie Standardparameter, die vom Compiler erzeugt werden, sollten überprüft werden, um die Signatur des Lambda-Ausdrucks oder der lokalen Funktion zu bestimmen. Auf diese Weise können Teams wie ASP.NET dasselbe Verhalten für Lambdas und lokale Funktionen wie normale Methoden zur Verfügung stellen.

Expliziter Rückgabetyp

Ein expliziter Rückgabetyp kann vor der Klammernparameterliste angegeben werden.

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?

Der Parser sieht voraus, um einen Methodenaufruf T() von einem Lambda-Ausdruck T () => ezu unterscheiden.

Explizite Rückgabetypen werden für anonyme Methoden, die mit delegate { } Syntax deklariert wurden, nicht unterstützt.

f = delegate int { return 1; };         // syntax error
f = delegate int (int x) { return x; }; // syntax error

Die Methodentypausleitung sollte eine genaue Ableitung von einem expliziten Lambda-Rückgabetyp vornehmen.

static void F<T>(Func<T, T> f) { ... }
F(int (i) => i); // Func<int, int>

Es sind keine Varianzkonvertierungen vom Lambda-Rückgabetyp zum Delegatrückgabetyp zulässig (dasselbe gilt auch für Parametertypen).

Func<object> f1 = string () => null; // error
Func<object?> f2 = object () => x;   // warning

Der Parser ermöglicht Lambda-Ausdrücke mit Rückgabetyp ref innerhalb von Ausdrücken ohne zusätzliche Klammern.

d = ref int () => x; // d = (ref int () => x)
F(ref int () => x);  // F((ref int () => x))

var kann nicht als expliziter Rückgabetyp für Lambda-Ausdrücke verwendet werden.

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

Natürlicher Typ (Funktion)

Eine anonyme Funktion Ausdruck (§12.19) (ein Lambda-Ausdruck oder eine anonyme Methode) hat einen natürlichen Typ, wenn die Parametertypen explizit sind und der Rückgabetyp entweder explizit oder abgeleitet werden kann (siehe §12.6.3.13).

Eine Methodengruppe hat einen natürlichen Typ, wenn alle Kandidatenmethoden in der Methodengruppe eine gemeinsame Signatur aufweisen. (Wenn die Methodengruppe Erweiterungsmethoden enthalten kann, gehören zu den Kandidaten auch der enthaltende Typ und alle Bereiche der Erweiterungsmethoden.)

Der natürliche Typ eines anonymen Funktionsausdrucks oder einer Methodengruppe ist eine function_type. Ein function_type stellt eine Methodensignatur dar: die Parametertypen und Verweisarten sowie der Rückgabetyp und die Verweisart. Anonyme Funktionsausdrücke oder Methodengruppen mit der gleichen Signatur haben den gleichen function_type.

Function_types werden nur in einigen bestimmten Kontexten verwendet:

  • implizite und explizite Konvertierungen
  • Methodentyp-Ableitung (§12.6.3) und am häufigsten verwendeten Typ (§12.6.3.15)
  • var-Initialisierer

Zur Kompilierungszeit existiert lediglich ein function_type: function_types erscheinen weder im Quellcode noch in den Metadaten.

Umwandlungen

Ausgehend von einem function_typeF können implizite function_type-Konvertierungen durchgeführt werden:

  • In einen function_typeG, wenn die Varianz der Parameter und Rückgabetypen von F in die Parameter und den Rückgabetyp von G konvertiert werden können.
  • In System.MulticastDelegate oder die Basisklassen oder Schnittstellen von System.MulticastDelegate
  • In System.Linq.Expressions.Expression oder System.Linq.Expressions.LambdaExpression

Anonyme Funktionsausdrücke und Methodengruppen verfügen bereits über Konvertierungen vom Ausdruck zu Delegattypen und Ausdrucksbaumstrukturtypen (siehe anonyme Funktionskonvertierungen §10.7 und Methodengruppenkonvertierungen §10.8). Diese Konvertierungen reichen für die Konvertierung in stark typierte Delegattypen und Ausdrucksbaumstrukturtypen aus. Die oben genannten function_type-Konvertierungen fügen nur Konvertierungen vom Typ zu den Basistypen hinzu: System.MulticastDelegate, System.Linq.Expressions.Expression usw.

Es gibt keine Konvertierungen zu einem Funktionstyp von einem anderen Typ als einem Funktionstyp. Es gibt keine expliziten Konvertierungen für function_types, da auf function_types nicht in der Quelle verwiesen werden kann.

Eine Konvertierung in System.MulticastDelegate oder Basistyp oder Schnittstelle realisiert die anonyme Funktion oder Methodengruppe als Instanz eines geeigneten Delegatentyps. Eine Konvertierung in denSystem.Linq.Expressions.Expression<TDelegate>- oder Basistyp stellt den Lambda-Ausdruck als Ausdrucksbaumstruktur mit einem geeigneten Delegattyp dar.

Delegate d = delegate (object obj) { }; // Action<object>
Expression e = () => "";                // Expression<Func<string>>
object o = "".Clone;                    // Func<object>

Function_type Konvertierungen sind keine impliziten oder expliziten Standardkonvertierungen §10.4 und werden nicht berücksichtigt, wenn ermittelt wird, ob ein benutzerdefinierter Konvertierungsoperator für eine anonyme Funktion oder Methodengruppe gilt. Aus auswertung der benutzerdefinierten Konvertierungen §10.5.3:

Damit ein Konvertierungsoperator anwendbar ist, muss es möglich sein, eine Standardkonvertierung (§10.4) vom Quelltyp in den Operandentyp des Operators durchzuführen, und es muss möglich sein, eine Standardkonvertierung vom Ergebnistyp des Operators in den Zieltyp durchzuführen.

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'

Es wird eine Warnung hinsichtlich einer impliziten Konvertierung einer Methodengruppe in objectgemeldet, da die Konvertierung gültig ist, möglicherweise aber nicht beabsichtigt war.

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

Typinferenz

Die bestehenden Regeln für typische Ableitungen sind größtenteils unverändert (siehe §12.6.3). Weiter unten sind jedoch einige Änderungen im Hinblick auf bestimmte Phasen der Typausleitung aufgeführt.

Erste Phase

In der ersten Phase (§12.6.3.2) kann eine anonyme Funktion an Ti gebunden werden, auch wenn Ti kein Delegat- oder Ausdrucksstrukturtyp ist (z. B. ein Typparameter, der auf System.Delegate beschränkt ist).

Für jedes der Methodenargumente Ei:

  • Wenn Ei eine anonyme Funktion ist und Ti ein Delegattyp oder Ausdrucksstrukturtypist, wird eine explizite Parametertypableitung von Ei zu Tierstellt, und eine explizite Rückgabetypableitung wird von Ei zu Tierstellt.
  • Wenn aber Ei einen Typ U hat und xi ein Wertparameter ist, dann wird ein UntergrenzenrückschlussvonU zuTi durchgeführt.
  • Andernfalls, wenn Ei einen Typ U hat und xi ein ref- oder out-Parameter ist, wird eine genaue AbleitungvonUzuTigemacht.
  • Andernfalls werden für dieses Argument keine Rückschlüsse gezogen.

Explizite Rückgabetyp-Ableitung

Ein expliziter Rückgabetyprückschluss wird von einem Ausdruck Ein einen Typ T folgendermaßen vorgenommen:

  • Wenn E eine anonyme Funktion mit explizitem Rückgabetyp Ur ist und T ein Delegat-Typ oder Ausdrucksbaumtyp mit Rückgabetyp Vr ist, wird eine exakte Ableitung (§12.6.3.9) vonUrzuVrgemacht.

Reparatur

Durch Festlegen (§12.6.3.12) wird sichergestellt, dass den function_type-Konvertierungen andere Konvertierungen vorgezogen werden. (Lambda-Ausdrücke und Methodengruppenausdrücke tragen nur zu niedrigeren Grenzen bei, sodass die Behandlung von function_types nur für Untergrenzen erforderlich ist.)

Eine nicht korrigierte Typvariable Xi mit einer Menge von Begrenzungen wird wie folgt korrigiert:

  • Die Gruppe der potenziellen TypenUj beginnt als Gruppe aller Typen in der Begrenzungsgruppe für Xi, wobei Funktionstypen an der unteren Grenze ignoriert werden, wenn andere Typen, die keine Funktionstypen sind, vorhanden sind.
  • Anschließend untersuchen wir wiederum jede Bindung für Xi: Für jede genaue Grenze U von Xi werden alle Typen Uj, die nicht mit U identisch sind, aus der Kandidatengruppe entfernt. Für jede untere Grenze U von Xi werden alle Typen Uj, in die keine implizite Konvertierung aus U vorhanden ist, aus der Kandidatenmenge entfernt. Für jede obere Grenze U von Xi werden alle Typen Uj, in die keine implizite Konvertierung in U vorhanden ist, aus der Kandidatengruppe entfernt.
  • Wenn es unter den verbleibenden Kandidatentypen Uj einen eindeutigen Typ V gibt, von dem aus eine implizite Konvertierung zu allen anderen Kandidatentypen möglich ist, wird Xi auf Vfestgesetzt.
  • Andernfalls schlägt die Typinferenz fehl.

Am häufigsten verwendete Typen

Der beste gemeinsame Typ (§12.6.3.15) wird in Bezug auf Typableitung definiert, sodass die oben genannten Änderungen bei der Typableitung auch auf den besten gemeinsamen Typ angewendet werden.

var fs = new[] { (string s) => s.Length, (string s) => int.Parse(s) }; // Func<string, int>[]

var

Anonyme Funktionen und Methodengruppen mit Funktionstypen können als Initialisierer in var Deklarationen verwendet werden.

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> 

Funktionstypen werden in Zuordnungen zum Verwerfen nicht verwendet.

d = () => 0; // ok
_ = () => 1; // error

Delegattypen

Der Delegattyp für die anonyme Funktion oder Methodengruppe mit Parametertypen P1, ..., Pn und Rückgabetyp R lautet:

  • wenn ein Parameter oder Rückgabewert nicht nach Wert ist oder mehr als 16 Parameter vorhanden sind oder eines der Parametertypen oder Rückgaben ungültige Typargumente sind (z. B. (int* p) => { }), ist der Delegat ein synthetisierter internal anonymer Delegattyp mit Signatur, der der anonymen Funktion oder Methodengruppe entspricht, und mit Parameternamen arg1, ..., argn oder arg, wenn ein einzelner Parameter vorhanden ist;
  • wenn Rvoidist, ist der Delegattyp System.Action<P1, ..., Pn>;
  • andernfalls ist der Delegattyp System.Func<P1, ..., Pn, R>.

Der Compiler kann zukünftig mehr Signaturen ermöglichen, sich an System.Action<>- und System.Func<>-Typen zu binden (wenn ref struct-Typen z.B. als Typargumente zulässig sind).

modopt() oder modreq() in der Methodengruppensignatur werden im entsprechenden Delegattyp ignoriert.

Wenn zwei anonyme Funktionen oder Methodengruppen in derselben Kompilierung synthetisierte Delegattypen mit denselben Parametertypen und Modifizierern und denselben Rückgabetyp und Modifizierern erfordern, verwendet der Compiler denselben synthetisierten Delegatentyp.

Überladungsauflösung

Das bessere Funktionsmember (§12.6.4.3) wird aktualisiert, damit es Member vorzieht, für die keine der beteiligten Konvertierungen und keines der Typenargumente Typen aus Lambda-Ausdrücken oder Methodengruppen abgeleitet hat.

Besseres Funktionselement

... Angesichts einer Argumentliste A mit einer Reihe von Argumentausdrücken {E1, E2, ..., En} und zwei anwendbaren Funktionsmembern Mp und Mq mit Parametertypen {P1, P2, ..., Pn} und {Q1, Q2, ..., Qn}, wird Mp als ein besseres Funktionsmember als Mq definiert, wenn

  1. die implizite Konvertierung von Ex in Px nicht für jedes Argument eine function_type_conversion ist, und
    • Mp eine nicht generische Methode ist, oder Mp eine generische Methode mit Typparametern {X1, X2, ..., Xp} ist und das Typargument für jeden Typparameter Xi aus einem Ausdruck oder einem anderen Typ als einem function_type abgeleitet wird, und
    • die implizite Konvertierung von Ex in Qx für mindestens ein Argument eine function_type_conversion ist, oder Mq eine generische Methode mit den Typparametern {Y1, Y2, ..., Yq} ist, und das Typargument für mindestens einen Typparameter Yi von einem function_type abgeleitet wird, oder
  2. für jedes Argument ist die implizite Konvertierung von Ex in Qx nicht besser als die implizite Konvertierung von Ex in Px, und für mindestens ein Argument ist die Konvertierung von Ex zu Px besser als die Konvertierung von Ex in Qx.

Eine bessere Konvertierung vom Ausdruck (§12.6.4.5) wurde aktualisiert, um Konvertierungen vorzuziehen, die keine von Lambdaausdrücken oder Methodengruppen abgeleiteten Typen beinhalten.

Bessere Konvertierung eines Ausdrucks

Aufgrund einer impliziten Konvertierung C1, die von einem Ausdruck E in einen Typ T1konvertiert wird, und einer impliziten Konvertierung C2, die von einem Ausdruck E in einen Typ T2konvertiert wird, ist C1 eine bessere Konvertierung als C2 wenn:

  1. C1 ist keine function_type_conversion und C2 ist eine function_type_conversion oder
  2. E ist eine nicht konstante interpolated_string_expression, C1 ist eine implicit_string_handler_conversion (Handlerkonvertierung einer impliziten Zeichenfolge), T1 ist ein applicable_interpolated_string_handler_type (anwendbarer Handler für implizite Zeichenfolgen), und C2 ist keine implicit_string_handler_conversion, oder
  3. E entspricht nicht genau T2 und erfüllt mindestens eine der folgenden Bedingungen:

Syntax

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?
  ;

Offene Probleme

Sollten Standardwerte für Lambda-Ausdrucksparameter zur Vollständigkeit unterstützt werden?

Sollte System.Diagnostics.ConditionalAttribute für Lambda-Ausdrücke verboten werden, da es nur wenige Szenarien gibt, in denen ein Lambda-Ausdruck bedingt verwendet werden könnte?

([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?

Sollte zusätzlich zum resultierenden Delegattyp der function_type über die Compiler-API verfügbar sein?

Derzeit verwendet der abgeleitete Delegattyp System.Action<> oder System.Func<>, wenn Parameter und Rückgabewerte gültige Typargumente und sind, es nicht mehr als 16 Parameter gibt und wenn der erwartete Typ Action<> oder Func<> fehlt, wird ein Fehler gemeldet. Sollte der Compiler stattdessen unabhängig von der Arität System.Action<> oder System.Func<> verwenden? Und wenn der erwartete Typ fehlt, andernfalls einen Delegattyp synthetisieren?