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:
- Consenti espressioni lambda con attributi
- Consenti espressioni lambda con tipo restituito esplicito
- 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_type
G
se i parametri e i tipi restituiti diF
sono convertibili in varianza nei parametri e restituiscono il tipo diG
- Per
System.MulticastDelegate
o classi o interfacce di base diSystem.MulticastDelegate
- Per
System.Linq.Expressions.Expression
oSystem.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.Expression
e 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 eTi
è un tipo delegato o un tipo di albero delle espressioni, viene eseguita un'inferenza esplicita del tipo di parametro daEi
aTi
e viene eseguita un'inferenza esplicita del tipo restituito daEi
aTi
.- In caso contrario, se
Ei
ha un tipoU
exi
è un parametro value, viene un di inferenza con limite inferiore daU
aTi
.- In caso contrario, se
Ei
ha un tipoU
exi
è un parametroref
oout
, viene eseguita un'di inferenza esatta daU
aTi
.- 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
E
a un tipoT
nel modo seguente:
- Se
E
è una funzione anonima con tipo restituito esplicitoUr
eT
è un tipo delegato o un tipo di albero delle espressioni con tipo restituitoVr
un 'inferenza esatta (§12.6.3.9) viene daUr
aVr
.
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 perXi
, 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 esattoU
diXi
, tutti i tipiUj
che non sono identici aU
vengono rimossi dall'insieme candidato. Per ogniU
con limite inferiore diXi
tutti i tipiUj
a cui non è una conversione implicita daU
vengono rimossi dal set di candidati. Per ogniU
limite superiore diXi
tutti i tipiUj
da cui è presente non una conversione implicita inU
vengono rimossi dal set candidato.- Se tra i tipi candidati rimanenti
Uj
esiste un tipo univocoV
da cui è presente una conversione implicita in tutti gli altri tipi candidati,Xi
viene risolto inV
.- 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 sintetizzatointernal
tipo delegato anonimo con firma corrispondente alla funzione o al gruppo di metodi anonimo e con nomi di parametriarg1, ..., argn
oarg
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 applicabiliMp
eMq
con tipi di parametro{P1, P2, ..., Pn}
e{Q1, Q2, ..., Qn}
,Mp
è definito come membro di funzione migliore rispetto aMq
se
- per ogni argomento, la conversione implicita da
Ex
aPx
non è un function_type_conversione
Mp
è un metodo non generico oMp
è un metodo generico con parametri di tipo{X1, X2, ..., Xp}
e per ogni parametro di tipoXi
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
aQx
è un function_type_conversionoMq
è un metodo generico con parametri di tipo{Y1, Y2, ..., Yq}
e per almeno un parametro di tipoYi
l'argomento di tipo viene dedotto da un function_typeo- per ogni argomento, la conversione implicita da
Ex
aQx
non è migliore della conversione implicita daEx
aPx
e per almeno un argomento, la conversione daEx
aPx
è migliore della conversione daEx
aQx
.
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'espressioneE
a un tipoT1
e unC2
di conversione implicita che esegue la conversione da un'espressioneE
a un tipoT2
,C1
è una conversione migliore rispetto aC2
se:
C1
non è un function_type_conversion eC2
è un function_type_conversionoE
è un interpolated_string_expressionnon costante,C1
è un implicit_string_handler_conversion,T1
è un applicable_interpolated_string_handler_typeeC2
non è un implicit_string_handler_conversionoE
non corrisponde esattamente aT2
e almeno uno delle seguenti condizioni è vero:
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?
C# feature specifications