Condividi tramite


Parametri facoltativi e di matrice di parametri per lambda e gruppi di metodi

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 acquisite nelle note language design meeting (LDM) pertinenti.

Puoi trovare ulteriori informazioni sul processo di adozione degli speclet di funzionalità nello standard del linguaggio C# nell'articolo sulle specifiche di .

Sommario

Per basarsi sui miglioramenti dell'espressione lambda introdotti in C# 10 (vedere in background pertinente), si propone di aggiungere il supporto per i valori dei parametri predefiniti e params matrici nelle espressioni lambda. In questo modo gli utenti possono implementare le espressioni lambda seguenti:

var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6

var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3

Analogamente, verrà consentito lo stesso tipo di comportamento per i gruppi di metodi:

var addWithDefault = AddWithDefaultMethod;
addWithDefault(); // 3
addWithDefault(5); // 6

var counter = CountMethod;
counter(); // 0
counter(1, 2); // 2

int AddWithDefaultMethod(int addTo = 2) {
  return addTo + 1;
}
int CountMethod(params int[] xs) {
  return xs.Length;
}

Sfondo pertinente

miglioramenti lambda in C# 10

Specifica di conversione del gruppo di metodi §10.8

Motivazione

I framework di app nell'ecosistema .NET sfruttano fortemente le espressioni lambda per consentire agli utenti di scrivere rapidamente la logica di business associata a un endpoint.

var app = WebApplication.Create(args);

app.MapPost("/todos/{id}", (TodoService todoService, int id, string task) => {
  var todo = todoService.Create(id, task);
  return Results.Created(todo);
});

Le espressioni lambda non supportano attualmente l'impostazione dei valori predefiniti per i parametri, quindi se uno sviluppatore vuole creare un'applicazione resiliente agli scenari in cui gli utenti non forniscono dati, è costretto a utilizzare funzioni locali o impostare i valori predefiniti all'interno del corpo lambda, anziché la sintassi proposta più concisa.

var app = WebApplication.Create(args);

app.MapPost("/todos/{id}", (TodoService todoService, int id, string task = "foo") => {
  var todo = todoService.Create(id, task);
  return Results.Created(todo);
});

La sintassi proposta ha anche il vantaggio di ridurre le differenze confuse tra le funzioni lambda e le funzioni locali, rendendo più semplice comprendere i costrutti e trasformare le espressioni lambda in funzioni senza compromettere le caratteristiche, in particolare in altri scenari in cui le espressioni lambda vengono utilizzate nelle API dove anche i gruppi di metodi possono essere forniti come riferimenti. Questa è anche la principale motivazione per supportare l'array params, che non è coperto dallo scenario del caso d'uso precedentemente menzionato.

Per esempio:

var app = WebApplication.Create(args);

Result TodoHandler(TodoService todoService, int id, string task = "foo") {
  var todo = todoService.Create(id, task);
  return Results.Created(todo);
}

app.MapPost("/todos/{id}", TodoHandler);

Comportamento precedente

Prima di C# 12, quando un utente implementa un'espressione lambda con un parametro facoltativo o params, il compilatore genera un errore.

var addWithDefault = (int addTo = 2) => addTo + 1; // error CS1065: Default values are not valid in this context.
var counter = (params int[] xs) => xs.Length; // error CS1670: params is not valid in this context

Quando un utente tenta di usare un gruppo di metodi in cui il metodo sottostante ha un parametro facoltativo o params, la tipologia di questi parametri non viene riportata, quindi la chiamata al metodo non supera il controllo del tipo a causa di una mancata corrispondenza nel numero di argomenti previsti.

void M1(int i = 1) { }
var m1 = M1; // Infers Action<int>
m1(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int>'

void M2(params int[] xs) { }
var m2 = M2; // Infers Action<int[]>
m2(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int[]>'

Nuovo comportamento

Dopo questa proposta (parte di C# 12), i valori predefiniti e params possono essere applicati ai parametri lambda con il comportamento seguente:

var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6

var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3

I valori predefiniti e i params possono essere applicati ai parametri del gruppo di metodi definendo in modo specifico tale gruppo di metodi:

int AddWithDefault(int addTo = 2) {
  return addTo + 1;
}

var add1 = AddWithDefault; 
add1(); // ok, default parameter value will be used

int Counter(params int[] xs) {
  return xs.Length;
}

var counter1 = Counter;
counter1(1, 2, 3); // ok, `params` will be used

Cambiamento critico

Prima di C# 12, il tipo dedotto di un gruppo di metodi è Action o Func in modo che il codice seguente venga compilato:

void WriteInt(int i = 0) {
  Console.Write(i);
}

var writeInt = WriteInt; // Inferred as Action<int>
DoAction(writeInt, 3); // Ok, writeInt is an Action<int>

void DoAction(Action<int> a, int p) {
  a(p);
}

int Count(params int[] xs) {
  return xs.Length;
}
var counter = Count; // Inferred as Func<int[], int>
DoFunction(counter, 3); // Ok, counter is a Func<int[], int>

int DoFunction(Func<int[], int> f, int p) {
  return f(new[] { p });
}

Dopo questa modifica (parte di C# 12), il codice di questa natura smette di compilare in .NET SDK 7.0.200 o versione successiva.

void WriteInt(int i = 0) {
  Console.Write(i);
}

var writeInt = WriteInt; // Inferred as anonymous delegate type
DoAction(writeInt, 3); // Error, cannot convert from anonymous delegate type to Action

void DoAction(Action<int> a, int p) {
  a(p);
}

int Count(params int[] xs) {
  return xs.Length;
}
var counter = Count; // Inferred as anonymous delegate type
DoFunction(counter, 3); // Error, cannot convert from anonymous delegate type to Func

int DoFunction(Func<int[], int> f, int p) {
  return f(new[] { p });
}

L'impatto di questo cambiamento critico deve essere considerato. Fortunatamente, l'uso di var per dedurre il tipo di un gruppo di metodi è stato supportato solo da C# 10, quindi solo il codice scritto da allora che si basa in modo esplicito su questo comportamento si interromperebbe.

Progettazione dettagliata

Modifiche di grammatica e parser

Questo miglioramento richiede le modifiche seguenti alla grammatica per le espressioni lambda.

 lambda_expression
   : modifier* identifier '=>' (block | expression)
-  | attribute_list* modifier* type? lambda_parameters '=>' (block | expression)
+  | attribute_list* modifier* type? lambda_parameter_list '=>' (block | expression)
   ;

+lambda_parameter_list
+  : lambda_parameters (',' parameter_array)?
+  | parameter_array
+  ;

 lambda_parameter
   : identifier
-  | attribute_list* modifier* type? identifier
+  | attribute_list* modifier* type? identifier default_argument?
   ;

Si noti che ciò consente valori di parametro predefiniti e matrici params solo per le espressioni lambda, non per i metodi anonimi dichiarati con sintassi delegate { }.

Le stesse regole dei parametri del metodo (§15.6.2) si applicano per i parametri lambda:

  • Un parametro con un modificatore ref, out o this non può avere un default_argument.
  • Un parameter_array può verificarsi dopo un parametro facoltativo, ma non può avere un valore predefinito. L'omissione di argomenti per un parameter_array comporta invece la creazione di una matrice vuota.

Non sono necessarie modifiche alla grammatica per i gruppi di metodi perché questa proposta cambierebbe solo la semantica.

L'aggiunta seguente (in grassetto) è necessaria per le conversioni di funzioni anonime (§10.7):

In particolare, una funzione anonima F è compatibile con un tipo delegato D fornito:

  • [...]
  • Se F ha un elenco di parametri tipizzato in modo esplicito, ogni parametro in D ha lo stesso tipo e modificatori del parametro corrispondente in Fignorando params modificatori e valori predefiniti.

Aggiornamenti delle proposte precedenti

L'aggiunta seguente (in grassetto) è necessaria alla specifica dei tipi di funzione nel in una proposta precedente:

Un gruppo di metodi ha un tipo naturale se tutti i metodi candidati nel gruppo di metodi hanno una firma comune inclusi i valori predefiniti e i modificatori params. 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: tipi di parametro, valori predefiniti, tipi di riferimento, modificatori paramse tipo restituito e tipo ref. Le espressioni di funzione anonime o i gruppi di metodi con la stessa firma hanno lo stesso function_type.

L'aggiunta seguente (in grassetto) è necessaria per la specifica dei tipi delegati in una proposta precedente.

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

  • se qualsiasi parametro o valore restituito non viene passato per valore, o qualsiasi parametro è facoltativo o params, oppure sono presenti più di 16 parametri, oppure uno qualsiasi dei tipi di parametro o restituito non è un argomento di tipo valido (ad esempio, (int* p) => { }), allora il delegato è un tipo di delegato anonimo sintetizzato internal con firma che corrisponde alla funzione anonima o al gruppo di metodi, e con nomi di parametro arg1, ..., argn o arg se è presente un solo parametro; [...]

Cambiamenti nel raccoglitore

Sintetizzare nuovi tipi di delegati

Come per il comportamento dei delegati con parametri ref o out, i tipi delegati vengono sintetizzati per espressioni lambda o gruppi di metodi definiti con parametri facoltativi o params. Si noti che negli esempi seguenti la notazione a', b'e così via viene usata per rappresentare questi tipi delegati anonimi.

var addWithDefault = (int addTo = 2) => addTo + 1;
// internal delegate int a'(int arg = 2);
var printString = (string toPrint = "defaultString") => Console.WriteLine(toPrint);
// internal delegate void b'(string arg = "defaultString");
var counter = (params int[] xs) => xs.Length;
// internal delegate int c'(params int[] arg);
string PathJoin(string s1, string s2, string sep = "/") { return $"{s1}{sep}{s2}"; }
var joinFunc = PathJoin;
// internal delegate string d'(string arg1, string arg2, string arg3 = " ");

Comportamento di conversione e unificazione

I delegati anonimi con parametri facoltativi verranno unificati quando lo stesso parametro (in base alla posizione) ha lo stesso valore predefinito, indipendentemente dal nome del parametro.

int E(int j = 13) {
  return 11;
}

int F(int k = 0) {
  return 3;
}

int G(int x = 13) {
  return 4;
}

var a = (int i = 13) => 1;
// internal delegate int b'(int arg = 13);
var b = (int i = 0) => 2;
// internal delegate int c'(int arg = 0);
var c = (int i = 13) => 3;
// internal delegate int b'(int arg = 13);
var d = (int c = 13) => 1;
// internal delegate int b'(int arg = 13);

var e = E;
// internal delegate int b'(int arg = 13);
var f = F;
// internal delegate int c'(int arg = 0);
var g = G;
// internal delegate int b'(int arg = 13);

a = b; // Not allowed
a = c; // Allowed
a = d; // Allowed
c = e; // Allowed
e = f; // Not Allowed
b = f; // Allowed
e = g; // Allowed

d = (int c = 10) => 2; // Warning: default parameter value is different between new lambda
                       // and synthesized delegate b'. We won't do implicit conversion

I delegati anonimi con una matrice come ultimo parametro verranno unificati quando l'ultimo parametro ha lo stesso modificatore params e il tipo di matrice, indipendentemente dal nome del parametro.

int C(int[] xs) {
  return xs.Length;
}

int D(params int[] xs) {
  return xs.Length;
}

var a = (int[] xs) => xs.Length;
// internal delegate int a'(int[] xs);
var b = (params int[] xs) => xs.Length;
// internal delegate int b'(params int[] xs);

var c = C;
// internal delegate int a'(int[] xs);
var d = D;
// internal delegate int b'(params int[] xs);

a = b; // Not allowed
a = c; // Allowed
b = c; // Not allowed
b = d; // Allowed

c = (params int[] xs) => xs.Length; // Warning: different delegate types; no implicit conversion
d = (int[] xs) => xs.Length; // OK. `d` is `delegate int (params int[] arg)`

Analogamente, esiste naturalmente la compatibilità con i delegati denominati che supportano già parametri facoltativi e params. Quando i valori predefiniti o i modificatori di params differiscono in una conversione, l'origine 1 verrà inutilizzata se si trova in un'espressione lambda, perché l'espressione lambda non può essere chiamata in altro modo. Potrebbe risultare controintuitivo per gli utenti, quindi verrà emesso un avviso quando il valore predefinito di origine o il modificatore params è presente e diverso da quello di destinazione. Se l'origine è un gruppo di metodi, può essere chiamata autonomamente, pertanto non verrà generato alcun avviso.

delegate int DelegateNoDefault(int x);
delegate int DelegateWithDefault(int x = 1);

int MethodNoDefault(int x) => x;
int MethodWithDefault(int x = 2) => x;
DelegateNoDefault d1 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d2 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d3 = MethodNoDefault; // no warning: source is a method group
DelegateNoDefault d4 = (int x = 1) => x; // warning: source present, target missing
DelegateWithDefault d5 = (int x = 2) => x; // warning: source present, target different
DelegateWithDefault d6 = (int x) => x; // no warning: source missing, target present

delegate int DelegateNoParams(int[] xs);
delegate int DelegateWithParams(params int[] xs);

int MethodNoParams(int[] xs) => xs.Length;
int MethodWithParams(params int[] xs) => xs.Length;
DelegateNoParams d7 = MethodWithParams; // no warning: source is a method group
DelegateWithParams d8 = MethodNoParams; // no warning: source is a method group
DelegateNoParams d9 = (params int[] xs) => xs.Length; // warning: source present, target missing
DelegateWithParams d10 = (int[] xs) => xs.Length; // no warning: source missing, target present

Comportamento di IL/runtime

I valori dei parametri predefiniti verranno generati nei metadati. Il codice IL per questa caratteristica sarà molto simile a quello generato per le espressioni lambda con parametri ref e out. Verrà generata una classe che eredita da System.Delegate o simile, e il metodo Invoke includerà direttive .param per impostare i valori di parametro predefiniti o System.ParamArrayAttribute, proprio come accadrebbe per un delegato denominato standard con parametri opzionali o params.

Questi tipi delegati possono essere controllati in fase di esecuzione, come di consueto. Nel codice, gli utenti possono analizzare il DefaultValue nel ParameterInfo associato al lambda o al gruppo di metodi utilizzando il MethodInfoassociato.

var addWithDefault = (int addTo = 2) => addTo + 1;
int AddWithDefaultMethod(int addTo = 2)
{
    return addTo + 1;
}

var defaultParm = addWithDefault.Method.GetParameters()[0].DefaultValue; // 2

var add1 = AddWithDefaultMethod;
defaultParm = add1.Method.GetParameters()[0].DefaultValue; // 2

Domande aperte

Nessuno di questi sono stati implementati. Rimangono proposte aperte.

Domanda aperta: come interagisce con l'attributo DefaultParameterValue esistente?

risposta proposta: Per coerenza, consentire l'attributo DefaultParameterValue nelle espressioni lambda e assicurarsi che il comportamento di generazione del delegato corrisponda ai valori predefiniti dei parametri supportati tramite la sintassi.

var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed

Domanda aperta: Prima, si noti che non rientra nell'ambito della proposta corrente, ma potrebbe essere opportuno discutere in futuro. Vogliamo supportare le impostazioni predefinite con parametri lambda tipizzati implicitamente? Cioè,

delegate void M1(int i = 3);
M1 m = (x = 3) => x + x; // Ok

delegate void M2(long i = 2);
M2 m = (x = 3.0) => ...; //Error: cannot convert implicitly from long to double

Questa inferenza porta ad alcuni problemi di conversione complicati che richiederebbero una maggiore discussione.

Qui sono disponibili anche considerazioni sulle prestazioni di analisi. Ad esempio, oggi il termine (x = non può mai essere l'inizio di un'espressione lambda. Se questa sintassi fosse consentita per i valori predefiniti di lambda, il parser avrebbe bisogno di una maggiore capacità di lookahead (scansionare fino a un token =>) per determinare se un termine è un'espressione lambda oppure no.

Riunioni di progettazione

  • LDM 2022-10-10: decisione di aggiungere il supporto per params nello stesso modo dei valori predefiniti dei parametri.