Condividi tramite


Miglioramenti lambda

Nota

Questo articolo è una specifica di funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.

Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono registrate nelle note pertinenti del language design meeting (LDM) .

Puoi ottenere maggiori informazioni sul processo di adozione delle speclet di funzionalità nello standard del linguaggio C# nell'articolo sulle specifiche di .

Problema campione: https://github.com/dotnet/csharplang/issues/4934

Sommario

Modifiche proposte:

  1. Consenti espressioni lambda con attributi
  2. Consenti espressioni lambda con tipo restituito esplicito
  3. Dedurre un tipo delegato naturale per lambda e gruppi di metodi

Motivazione

Il supporto per gli attributi nelle espressioni lambda offre parità con i metodi e le funzioni locali.

Il supporto per i tipi restituiti espliciti fornisce la simmetria con parametri lambda in cui è possibile specificare tipi espliciti. Consentire tipi di ritorno espliciti fornisce anche un controllo delle prestazioni del compilatore nelle lambde annidate, in cui la risoluzione dell'overload deve attualmente vincolare il corpo della lambda per determinare la firma.

Un tipo naturale per espressioni lambda e gruppi di metodi consentirà più scenari in cui le espressioni lambda e i gruppi di metodi possono essere usati senza un tipo delegato esplicito, inclusi gli inizializzatori nelle dichiarazioni di var.

La richiesta di tipi delegati espliciti per le espressioni lambda e i gruppi di metodi è stato un punto di attrito per i clienti ed è diventato un ostacolo al progresso in ASP.NET con i recenti lavori su MapAction.

ASP.NET MapAction senza modifiche suggerite (MapAction() accetta un argomento 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 con tipi naturali per i gruppi di metodi:

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

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

ASP.NET MapAction con attributi e tipi naturali per le espressioni lambda:

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

Attributi

Gli attributi possono essere aggiunti alle espressioni lambda e ai parametri lambda. Per evitare ambiguità tra attributi del metodo e attributi dei parametri, un'espressione lambda con attributi deve usare un elenco di parametri racchiusi tra parentesi. I tipi di parametro non sono obbligatori.

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

È possibile specificare più attributi, separati da virgole all'interno dello stesso elenco di attributi o come elenchi di attributi separati.

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

Gli attributi non sono supportati per metodi anonimi dichiarati con delegate { } sintassi.

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

Il parser esaminerà in anticipo per distinguere un inizializzatore di raccolta con un'assegnazione di elemento da un inizializzatore di raccolta con un'espressione lambda.

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

Il parser considererà ?[ come inizio di un accesso condizionale agli elementi.

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

Gli attributi dell'espressione lambda o dei parametri lambda verranno emessi nei metadati sul metodo che corrisponde alla lambda.

In generale, i clienti non devono dipendere dal modo in cui le espressioni lambda e le funzioni locali eseguono il mapping dall'origine ai metadati. Il modo in cui le espressioni lambda e le funzioni locali possono essere generate è stato, e può ancora, cambiare tra le versioni del compilatore.

Le modifiche proposte di seguito sono destinate allo scenario basato su Delegate. Deve essere valido esaminare le MethodInfo associate a un'istanza di Delegate per determinare la firma dell'espressione lambda o della funzione locale, inclusi gli attributi espliciti e i metadati aggiuntivi generati dal compilatore, ad esempio i parametri predefiniti. In questo modo i team, ad esempio ASP.NET, possono rendere disponibili gli stessi comportamenti per le espressioni lambda e le funzioni locali dei metodi ordinari.

Tipo restituito esplicito

È possibile specificare un tipo restituito esplicito prima dell'elenco di parametri tra parentesi.

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?

Il parser esaminerà in anticipo per distinguere una chiamata di metodo T() da un'espressione lambda T () => e.

I tipi restituiti espliciti non sono supportati per i metodi anonimi dichiarati con sintassi delegate { }.

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

L'inferenza del tipo di metodo deve effettuare un'inferenza esatta a partire da un tipo di ritorno lambda esplicito.

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

Le conversioni di varianza non sono consentite dal tipo restituito lambda al tipo restituito delegato (corrispondente a un comportamento simile per i tipi di parametro).

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

Il parser consente espressioni lambda di tipo restituito ref all'interno di espressioni senza parentesi aggiuntive.

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

var non può essere usato come tipo restituito esplicito per le espressioni 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

Tipo naturale (funzione)

Una funzione anonima expression (§12.19) (un'espressione lambda o un metodo anonimo ) ha un tipo naturale se i tipi di parametri sono espliciti e il tipo restituito è esplicito o può essere dedotto (vedere §12.6.3.13).

Un gruppo di metodi ha un tipo naturale se tutti i metodi candidati nel gruppo di metodi hanno una firma comune. Se il gruppo di metodi può includere metodi di estensione, i candidati includono il tipo contenitore e tutti gli ambiti del metodo di estensione.

Il tipo naturale di un'espressione di funzione anonima o di un gruppo di metodi è un function_type. Un function_type rappresenta una firma del metodo: i tipi di parametro e i tipi ref e il tipo restituito e il tipo di riferimento. Le espressioni di funzione anonime o i gruppi di metodi con la stessa firma hanno lo stesso function_type.

Function_types vengono usati solo in alcuni contesti specifici:

  • conversioni implicite ed esplicite
  • inferenza del tipo di metodo (§12.6.3) e tipo comune migliore (§12.6.3.15)
  • inizializzatori var

Esiste un tipo_di_funzione solo durante la fase di compilazione: i tipi_di_funzione non appaiono nel codice sorgente o nei metadati.

Conversioni

Da un function_typeF sono presenti conversioni implicite di function_type:

  • A un function_typeG se i parametri e i tipi restituiti di F sono convertibili in varianza nei parametri e restituiscono il tipo di G
  • Per System.MulticastDelegate o classi o interfacce di base di System.MulticastDelegate
  • Per System.Linq.Expressions.Expression o System.Linq.Expressions.LambdaExpression

Le espressioni di funzione anonime e i gruppi di metodi hanno già conversioni da espressioni a tipi delegati e a tipi di albero delle espressioni (vedere conversioni di funzioni anonime §10.7 e conversioni di gruppi di metodi §10.8). Tali conversioni sono sufficienti per la conversione in tipi delegati fortemente tipizzati e tipi di alberi delle espressioni. Le conversioni di function_type precedenti aggiungono conversioni dal tipo solo ai tipi di base: System.MulticastDelegate, System.Linq.Expressions.Expressione così via.

Non sono presenti conversioni in un function_type da un tipo diverso da un function_type. Non esistono conversioni esplicite per function_types perché non è possibile fare riferimento a function_types nell'origine.

Una conversione in System.MulticastDelegate o tipo o interfaccia di base realizza la funzione o il gruppo di metodi anonimo come istanza di un tipo delegato appropriato. Una conversione in System.Linq.Expressions.Expression<TDelegate> o tipo di base realizza l'espressione lambda come albero delle espressioni con un tipo delegato appropriato.

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

Function_type conversioni non sono conversioni standard implicite o esplicite §10.4 e non vengono considerate quando si determina se un operatore di conversione definito dall'utente è applicabile a una funzione o a un gruppo di metodi anonimo. Dalla valutazione delle conversioni definite dall'utente §10.5.3:

Affinché un operatore di conversione sia applicabile, deve essere possibile eseguire una conversione standard (§10.4) dal tipo di origine al tipo di operando dell'operatore e deve essere possibile eseguire una conversione standard dal tipo di risultato dell'operatore al tipo di destinazione.

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'

Viene segnalato un avviso per una conversione implicita di un gruppo di metodi in object, perché la conversione è valida ma forse non intenzionale.

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

Inferenza di tipo

Le regole esistenti per l'inferenza del tipo sono principalmente invariate (vedere §12.6.3). Esistono tuttavia un paio di modifiche di seguito a fasi specifiche di inferenza del tipo.

Prima fase

La prima fase (§12.6.3.2) consente a una funzione anonima di eseguire l'associazione a Ti anche se Ti non è un delegato o un tipo di albero delle espressioni (ad esempio un parametro di tipo vincolato a System.Delegate).

Per ogni argomento del metodo Ei:

  • Se Ei è una funzione anonima e Ti è un tipo delegato o un tipo di albero delle espressioni, viene eseguita un'inferenza esplicita del tipo di parametro da Ei a Tie viene eseguita un'inferenza esplicita del tipo restituito da Ei a Ti.
  • In caso contrario, se Ei ha un tipo U e xi è un parametro value, viene un di inferenza con limite inferiore daUaTi.
  • In caso contrario, se Ei ha un tipo U e xi è un parametro ref o out, viene eseguita un'di inferenza esatta daUaTi.
  • In caso contrario, non viene eseguita alcuna inferenza per questo argomento.

inferenza esplicita del tipo di ritorno

Viene un'inferenza esplicita del tipo restituito da un'espressione Ea un tipo T nel modo seguente:

  • Se E è una funzione anonima con tipo restituito esplicito Ur e T è un tipo delegato o un tipo di albero delle espressioni con tipo restituito Vr un 'inferenza esatta (§12.6.3.9) viene daUraVr.

Correzione

Correzione (§12.6.3.12) garantisce che le altre conversioni siano preferite rispetto alle conversioni function_type. Le espressioni lambda e le espressioni del gruppo di metodi contribuiscono solo ai limiti inferiori, quindi la gestione di function_types è necessaria solo per i limiti inferiori.

Una variabile di tipo non fissata Xi con un set di limiti è fissata come indicato di seguito:

  • Il set di tipi candidati Uj inizia come l'insieme di tutti i tipi nei limiti per Xi, in cui i tipi di funzione vengono ignorati nei limiti inferiori se sono presenti tipi che non sono tipi di funzione.
  • Esaminiamo quindi ogni limite per Xi uno per uno: per ogni limite esatto U di Xi, tutti i tipi Uj che non sono identici a U vengono rimossi dall'insieme candidato. Per ogni U con limite inferiore di Xi tutti i tipi Uj a cui non è una conversione implicita da U vengono rimossi dal set di candidati. Per ogni U limite superiore di Xi tutti i tipi Uj da cui è presente non una conversione implicita in U vengono rimossi dal set candidato.
  • Se tra i tipi candidati rimanenti Uj esiste un tipo univoco V da cui è presente una conversione implicita in tutti gli altri tipi candidati, Xi viene risolto in V.
  • In caso contrario, l'inferenza del tipo ha esito negativo.

Tipo comune migliore

Il tipo comune migliore (§12.6.3.15) è definito in termini di inferenza del tipo in modo che le modifiche all'inferenza del tipo sopra si applichino anche al tipo comune migliore.

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

var

Le funzioni anonime e i gruppi di metodi con tipi di funzione possono essere usati come inizializzatori nelle dichiarazioni di 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> 

I tipi di funzione non vengono usati nelle assegnazioni agli scarti.

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

Tipi delegati

Il tipo delegato per la funzione anonima o il gruppo di metodi con tipi di parametro P1, ..., Pn e tipo restituito R è:

  • se un parametro o un valore restituito non è per valore o sono presenti più di 16 parametri o uno qualsiasi dei tipi di parametro o restituiti non sono argomenti di tipo validi (ad esempio, (int* p) => { }), il delegato è un tipo delegato sintetizzato internal tipo delegato anonimo con firma corrispondente alla funzione o al gruppo di metodi anonimo e con nomi di parametri arg1, ..., argn o arg se un singolo parametro;
  • se R è void, il tipo delegato è System.Action<P1, ..., Pn>;
  • in caso contrario, il tipo delegato è System.Func<P1, ..., Pn, R>.

Il compilatore potrebbe consentire l'associazione di più firme ai tipi System.Action<> e System.Func<> in futuro (se i tipi ref struct sono consentiti come argomenti di tipo, ad esempio).

modopt() o modreq() nella firma del gruppo di metodi vengono ignorati nel tipo delegato corrispondente.

Se due funzioni anonime o gruppi di metodi nella stessa compilazione richiedono tipi delegati sintetizzati con gli stessi tipi di parametro e modificatori e lo stesso tipo restituito e modificatori, il compilatore userà lo stesso tipo delegato sintetizzato.

Risoluzione del sovraccarico

Membro di funzione migliore (§12.6.4.3) viene aggiornato per preferire i membri in cui nessuna delle conversioni e nessuno degli argomenti di tipo coinvolti deduce tipi da espressioni lambda o gruppi di metodi.

Membro di funzione migliore

... Dato un elenco di argomenti A con un set di espressioni di argomento {E1, E2, ..., En} e due membri di funzione applicabili Mp e Mq con tipi di parametro {P1, P2, ..., Pn} e {Q1, Q2, ..., Qn}, Mp è definito come membro di funzione migliore rispetto a Mq se

  1. per ogni argomento, la conversione implicita da Ex a Px non è un function_type_conversione
    • Mp è un metodo non generico o Mp è un metodo generico con parametri di tipo {X1, X2, ..., Xp} e per ogni parametro di tipo Xi l'argomento di tipo viene dedotto da un'espressione o da un tipo diverso da un function_typee
    • per almeno un argomento, la conversione implicita da Ex a Qx è un function_type_conversiono Mq è un metodo generico con parametri di tipo {Y1, Y2, ..., Yq} e per almeno un parametro di tipo Yi l'argomento di tipo viene dedotto da un function_typeo
  2. per ogni argomento, la conversione implicita da Ex a Qx non è migliore della conversione implicita da Ex a Pxe per almeno un argomento, la conversione da Ex a Px è migliore della conversione da Ex a Qx.

Una migliore conversione dall'espressione (§12.6.4.5) viene aggiornata per preferire le conversioni che non comportavano tipi dedotti da espressioni lambda o gruppi di metodi.

Conversione migliore dall'espressione

Dato un C1 di conversione implicita che esegue la conversione da un'espressione E a un tipo T1e un C2 di conversione implicita che esegue la conversione da un'espressione E a un tipo T2, C1 è una conversione migliore rispetto a C2 se:

  1. C1 non è un function_type_conversion e C2 è un function_type_conversiono
  2. E è un interpolated_string_expressionnon costante, C1 è un implicit_string_handler_conversion, T1 è un applicable_interpolated_string_handler_typee C2 non è un implicit_string_handler_conversiono
  3. E non corrisponde esattamente a T2 e almeno uno delle seguenti condizioni è vero:
    • E corrisponde esattamente T1 (§12.6.4.5)
    • T1 è una destinazione di conversione migliore rispetto a T2 (§12.6.4.7)

Sintassi

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

Problemi aperti

I valori predefiniti devono essere supportati per i parametri dell'espressione lambda per la completezza?

È opportuno vietare l'uso di System.Diagnostics.ConditionalAttribute nelle espressioni lambda poiché ci sono pochi scenari in cui un'espressione lambda potrebbe essere usata in modo condizionale?

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

Il function_type deve essere disponibile dall'API del compilatore, oltre al tipo delegato risultante?

Attualmente, il tipo delegato dedotto usa System.Action<> o System.Func<> quando i tipi parametro e restituiti sono argomenti di tipo validi e non sono presenti più di 16 parametri e se il tipo di Action<> o Func<> previsto è mancante, viene segnalato un errore. Dovrebbe invece il compilatore usare System.Action<> o System.Func<> indipendentemente dall'arità? E se il tipo previsto è mancante, sintetizzare un tipo delegato in caso contrario?