Espressioni lambda e funzioni anonime
Per creare una funzione anonima, si usa un'espressione lambda . Usare l'operatore di dichiarazione lambda =>
per separare l'elenco di parametri dell'espressione lambda dal relativo corpo. Un'espressione lambda può essere di una delle due forme seguenti:
Espressione lambda con un'espressione nel corpo:
(input-parameters) => expression
'istruzione lambda con un blocco di istruzioni come corpo:
(input-parameters) => { <sequence-of-statements> }
Per creare un'espressione lambda, specificare i parametri di input (se presenti) sul lato sinistro dell'operatore lambda e un'espressione o un blocco di istruzioni sull'altro lato.
Qualsiasi espressione lambda può essere convertita in un tipo delegato . I tipi dei parametri e il valore restituito definiscono il tipo delegato in cui è possibile convertire un'espressione lambda. Se un'espressione lambda non restituisce un valore, può essere convertita in uno dei tipi delegati Action
; in caso contrario, può essere convertito in uno dei tipi delegati Func
. Ad esempio, un'espressione lambda con due parametri che non restituisce alcun valore può essere convertita in un delegato Action<T1,T2>. Un'espressione lambda con un parametro e restituisce un valore può essere convertita in un delegato Func<T,TResult>. Nell'esempio seguente, l'espressione lambda x => x * x
, che specifica un parametro denominato x
e restituisce il valore di x
quadrato, viene assegnato a una variabile di un tipo delegato:
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25
Le espressioni lambda possono anche essere convertite nei tipi di alberi delle espressioni , come illustrato nell'esempio seguente:
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)
Le espressioni lambda vengono usate in qualsiasi codice che richiede istanze di tipi delegati o alberi delle espressioni. Un esempio è l'argomento del metodo Task.Run(Action) per trasmettere il codice che deve essere eseguito in background. È anche possibile usare espressioni lambda quando si scrive LINQ in C#, come illustrato nell'esempio seguente:
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25
Quando si usa la sintassi basata su metodo per chiamare il metodo Enumerable.Select nella classe System.Linq.Enumerable, ad esempio in LINQ to Objects e LINQ to XML, il parametro è un tipo delegato System.Func<T,TResult>. Quando si chiama il metodo Queryable.Select nella classe System.Linq.Queryable, ad esempio in LINQ to SQL, il tipo di parametro è un tipo di albero delle espressioni Expression<Func<TSource,TResult>>
. In entrambi i casi, è possibile usare la stessa espressione lambda per specificare il valore del parametro. Ciò fa sì che le due chiamate Select
abbiano un aspetto simile, anche se in realtà il tipo di oggetti creati dalle espressioni lambda è diverso.
Espressioni lambda
Un'espressione lambda con un'espressione alla destra dell'operatore =>
viene chiamata espressione lambda . Un'espressione lambda restituisce il risultato dell'espressione e assume la forma di base seguente:
(input-parameters) => expression
Il corpo di un'espressione lambda può essere costituito da una chiamata al metodo. Tuttavia, se si creano alberi delle espressioni valutati al di fuori del contesto di .NET Common Language Runtime (CLR), ad esempio in SQL Server, non è consigliabile usare chiamate al metodo nelle espressioni lambda. I metodi non hanno alcun significato al di fuori del contesto di .NET Common Language Runtime (CLR).
Espressioni lambda delle istruzioni
Una dichiarazione lambda assomiglia a un'espressione lambda, tranne per il fatto che le sue istruzioni sono racchiuse tra parentesi graffe:
(input-parameters) => { <sequence-of-statements> }
Il corpo di un'istruzione lambda può essere costituito da un numero qualsiasi di istruzioni; tuttavia, in pratica di solito non ci sono più di due o tre.
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!
Non è possibile usare lambda di istruzione per creare alberi di espressioni.
Parametri di input di un'espressione lambda
I parametri di input di un'espressione lambda sono racchiusi tra parentesi. Specificare i parametri di input come zero, utilizzando parentesi vuote:
Action line = () => Console.WriteLine();
Se un'espressione lambda ha un solo parametro di input, le parentesi sono facoltative:
Func<double, double> cube = x => x * x * x;
Due o più parametri di input sono separati da virgole:
Func<int, int, bool> testForEquality = (x, y) => x == y;
In alcuni casi il compilatore non può dedurre i tipi di parametri di input. È possibile specificare i tipi in modo esplicito, come illustrato nell'esempio seguente:
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
I tipi di parametro di input devono essere tutti espliciti o tutti impliciti; in caso contrario, si verifica un errore del compilatore CS0748
È possibile usare elimina per specificare due o più parametri di input di un'espressione lambda non usati nell'espressione:
Func<int, int, int> constant = (_, _) => 42;
I parametri di eliminazione lambda possono essere utili quando si usa un'espressione lambda per fornire un gestore eventi.
Nota
Per la compatibilità con le versioni precedenti, se solo un singolo parametro di input è denominato _
, quindi, all'interno di un'espressione lambda, _
viene considerato come il nome di tale parametro.
A partire da C# 12, è possibile fornire valori predefiniti per i parametri nelle espressioni lambda. La sintassi e le restrizioni sui valori dei parametri predefiniti sono uguali a per i metodi e le funzioni locali. L'esempio seguente dichiara un'espressione lambda con un parametro predefinito, quindi la chiama una volta usando l'impostazione predefinita e una volta con due parametri espliciti:
var IncrementBy = (int source, int increment = 1) => source + increment;
Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7
È anche possibile dichiarare espressioni lambda con params
matrici o raccolte come parametri:
var sum = (params IEnumerable<int> values) =>
{
int sum = 0;
foreach (var value in values)
sum += value;
return sum;
};
var empty = sum();
Console.WriteLine(empty); // 0
var sequence = new[] { 1, 2, 3, 4, 5 };
var total = sum(sequence);
Console.WriteLine(total); // 15
Come parte di questi aggiornamenti, quando un gruppo di metodi con un parametro predefinito viene assegnato a un'espressione lambda, tale espressione lambda ha anche lo stesso parametro predefinito. È anche possibile assegnare un gruppo di metodi con un parametro di raccolta params
a un'espressione lambda.
Le espressioni lambda con parametri predefiniti o raccolte di params
come parametri non hanno tipi naturali che corrispondono ai tipi Func<>
o Action<>
. Tuttavia, è possibile definire tipi delegati che includono valori di parametro predefiniti:
delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);
delegate int SumCollectionDelegate(params IEnumerable<int> values);
In alternativa, è possibile usare variabili tipizzate in modo implicito con dichiarazioni di var
per definire il tipo delegato. Il compilatore sintetizza il tipo di delegato corretto.
Per altre informazioni sui parametri predefiniti nelle espressioni lambda, vedere la specifica di funzionalità per parametri predefiniti nelle espressioni lambda.
Espressioni lambda asincrone
È possibile creare facilmente espressioni lambda e istruzioni che incorporano l'elaborazione asincrona usando le parole chiave async e await. L'esempio di Windows Form seguente, ad esempio, contiene un gestore eventi che chiama e attende un metodo asincrono, ExampleMethodAsync
.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += button1_Click;
}
private async void button1_Click(object sender, EventArgs e)
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
È possibile aggiungere lo stesso gestore eventi usando una lambda asincrona. Per aggiungere questo gestore, aggiungere un modificatore async
prima dell'elenco di parametri lambda, come illustrato nell'esempio seguente:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += async (sender, e) =>
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
};
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
Per saperne di più riguardo a come creare e usare metodi asincroni, vedere Programmazione asincrona con async e await.
Espressioni lambda e tuple
Il linguaggio C# offre supporto predefinito per le tuple . È possibile fornire una tupla come argomento a un'espressione lambda e l'espressione lambda può anche restituire una tupla. In alcuni casi, il compilatore C# usa l'inferenza dei tipi per determinare le componenti della tupla.
Per definire una tupla, racchiudere tra parentesi un elenco delimitato da virgole dei relativi componenti. Nell'esempio seguente viene utilizzata la tupla con tre componenti per passare una sequenza di numeri a un'espressione lambda, che raddoppia ogni valore e restituisce una tupla con tre componenti che contengono il risultato delle moltiplicazioni.
Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)
In genere, i campi di una tupla sono denominati Item1
, Item2
e così via. È tuttavia possibile definire una tupla con componenti denominati, come illustrato nell'esempio seguente.
Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
Per ulteriori informazioni sulle tuple C#, vedere tipi di tuple.
Funzioni lambda con gli operatori di query standard
LINQ to Objects, tra le altre implementazioni, ha un parametro di input il cui tipo è uno della famiglia di delegati generici Func<TResult>. Questi delegati usano parametri di tipo per definire il numero e il tipo di parametri di input e il tipo di ritorno del delegato. I delegati Func
sono utili per racchiudere espressioni definite dall'utente applicate a ogni elemento in un set di dati di origine. Si consideri, ad esempio, il tipo delegato Func<T,TResult>:
public delegate TResult Func<in T, out TResult>(T arg)
Il delegato può essere istanziato come un'istanza di Func<int, bool>
in cui int
è un parametro di input e bool
è il valore restituito. Il valore restituito viene sempre specificato nell'ultimo parametro di tipo. Ad esempio, Func<int, string, bool>
definisce un delegato con due parametri di input, int
e string
, e un tipo di ritorno di bool
. Il seguente Func
delegato, quando richiamato, restituisce un valore booleano che indica se il parametro di input è uguale a cinque:
Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result); // False
È anche possibile fornire un'espressione lambda quando il tipo di argomento è un Expression<TDelegate>, ad esempio negli operatori di query standard definiti nel tipo Queryable. Quando si specifica un argomento Expression<TDelegate>, l'espressione lambda viene compilata in un albero delle espressioni.
L'esempio seguente usa l'operatore di query standard Count:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");
Il compilatore può dedurre il tipo del parametro di input oppure è anche possibile specificarlo in modo esplicito. Questa particolare espressione lambda conta i numeri interi (n
) che, se divisi per due, hanno un resto di 1.
L'esempio seguente genera una sequenza che contiene tutti gli elementi nella matrice numbers
che precede 9, perché è il primo numero della sequenza che non soddisfa la condizione:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3
Nell'esempio seguente vengono specificati più parametri di input racchiudendoli tra parentesi. Il metodo restituisce tutti gli elementi nella matrice numbers
finché non trova un numero il cui valore è minore della posizione ordinale nella matrice:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4
Non si usano espressioni lambda direttamente nelle espressioni di query , ma è possibile usarle nelle chiamate di metodo all'interno delle espressioni di query, come illustrato nell'esempio seguente:
var numberSets = new List<int[]>
{
new[] { 1, 2, 3, 4, 5 },
new[] { 0, 0, 0 },
new[] { 9, 8 },
new[] { 1, 0, 1, 0, 1, 0, 1, 0 }
};
var setsWithManyPositives =
from numberSet in numberSets
where numberSet.Count(n => n > 0) > 3
select numberSet;
foreach (var numberSet in setsWithManyPositives)
{
Console.WriteLine(string.Join(" ", numberSet));
}
// Output:
// 1 2 3 4 5
// 1 0 1 0 1 0 1 0
Inferenza dei tipi nelle espressioni lambda
Quando si scrivono espressioni lambda, spesso non è necessario specificare un tipo per i parametri di input perché il compilatore può dedurre il tipo in base al corpo lambda, ai tipi di parametro e ad altri fattori, come descritto nella specifica del linguaggio C#. Per la maggior parte degli operatori di query standard, il primo input è il tipo degli elementi nella sequenza di origine. Se si esegue una query su un IEnumerable<Customer>
, la variabile di input viene dedotta come oggetto Customer
, il che significa che è possibile accedere ai relativi metodi e proprietà:
customers.Where(c => c.City == "London");
Le regole generali per l'inferenza del tipo per le espressioni lambda sono le seguenti:
- L'espressione lambda deve contenere lo stesso numero di parametri del tipo delegato.
- Ogni parametro di input nell'espressione lambda deve essere convertibile in modo implicito nel parametro delegato corrispondente.
- Il valore restituito di un'espressione lambda (se presente) deve poter essere convertito implicitamente nel tipo di ritorno del delegato.
Tipo naturale di un'espressione lambda
Un'espressione lambda in sé non ha un tipo perché il sistema di tipi comune non ha un concetto intrinseco di "espressione lambda". Tuttavia, a volte è utile parlare in modo informale del "tipo" di un'espressione lambda. Quel "tipo" informale si riferisce al tipo delegato oppure al tipo Expression a cui l'espressione lambda viene convertita.
Un'espressione lambda può avere un tipo naturale . Anziché forzare la dichiarazione di un tipo delegato, ad esempio Func<...>
o Action<...>
per un'espressione lambda, il compilatore può dedurre il tipo delegato dall'espressione lambda. Si consideri ad esempio la dichiarazione seguente:
var parse = (string s) => int.Parse(s);
Il compilatore può dedurre che parse
è un Func<string, int>
. Il compilatore sceglie un delegato Func
o Action
disponibile, se ne esiste uno adatto. In caso contrario, sintetizza un tipo delegato. Ad esempio, il tipo delegato viene sintetizzato se l'espressione lambda ha ref
parametri. Quando un'espressione lambda ha un tipo naturale, può essere assegnata a un tipo meno esplicito, ad esempio System.Object o System.Delegate:
object parse = (string s) => int.Parse(s); // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>
I gruppi di metodi (ovvero i nomi dei metodi senza elenchi di parametri) con esattamente un solo sovraccarico assumono un tipo naturale:
var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose
Se si assegna un'espressione lambda a System.Linq.Expressions.LambdaExpressiono System.Linq.Expressions.Expressione l'espressione lambda ha un tipo delegato naturale, l'espressione ha un tipo naturale di System.Linq.Expressions.Expression<TDelegate>, con il tipo delegato naturale usato come argomento per il parametro di tipo:
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Non tutte le espressioni lambda hanno un tipo naturale. Si consideri la dichiarazione seguente:
var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda
Il compilatore non può dedurre un tipo di parametro per s
. Quando il compilatore non può dedurre un tipo naturale, è necessario dichiarare il tipo:
Func<string, int> parse = s => int.Parse(s);
Tipo di ritorno esplicito
In genere, il tipo restituito di un'espressione lambda è ovvio e dedotto. Per alcune espressioni che non funzionano:
var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type
È possibile specificare il tipo restituito di un'espressione lambda prima dei parametri di input. Quando si specifica un tipo restituito esplicito, è necessario inserire tra parentesi i parametri di input:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Attributi
È possibile aggiungere attributi a un'espressione lambda e ai relativi parametri. Nell'esempio seguente viene illustrato come aggiungere attributi a un'espressione lambda:
Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;
È anche possibile aggiungere attributi ai parametri di input o al valore restituito, come illustrato nell'esempio seguente:
var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;
Come illustrato negli esempi precedenti, è necessario inserire tra parentesi i parametri di input quando si aggiungono attributi a un'espressione lambda o ai relativi parametri.
Importante
Le espressioni lambda vengono richiamate tramite il tipo delegato sottostante. È diverso dai metodi e dalle funzioni locali. Il metodo Invoke
del delegato non controlla gli attributi nell'espressione lambda. Gli attributi non hanno alcun effetto quando viene richiamata l'espressione lambda. Gli attributi sulle espressioni lambda sono utili per l'analisi del codice e possono essere individuati tramite riflessione. Una conseguenza di questa decisione è che il System.Diagnostics.ConditionalAttribute non può essere applicato a un'espressione lambda.
Acquisizione di variabili esterne e ambito delle variabili nelle espressioni lambda
Le lambda possono fare riferimento a variabili esterne. Queste variabili esterne sono le variabili visibili nel metodo che definisce l'espressione lambda o visibili nel tipo che contiene l'espressione lambda. Le variabili acquisite in questo modo vengono archiviate per l'uso nell'espressione lambda anche se altrimenti le variabili escono dal contesto e vengono sottoposte a gestione della memoria. Prima che una variabile esterna possa essere utilizzata in un'espressione lambda, deve essere assegnata in modo definitivo. L'esempio seguente illustra queste regole:
public static class VariableScopeWithLambdas
{
public class VariableCaptureGame
{
internal Action<int>? updateCapturedLocalVariable;
internal Func<int, bool>? isEqualToCapturedLocalVariable;
public void Run(int input)
{
int j = 0;
updateCapturedLocalVariable = x =>
{
j = x;
bool result = j > input;
Console.WriteLine($"{j} is greater than {input}: {result}");
};
isEqualToCapturedLocalVariable = x => x == j;
Console.WriteLine($"Local variable before lambda invocation: {j}");
updateCapturedLocalVariable(10);
Console.WriteLine($"Local variable after lambda invocation: {j}");
}
}
public static void Main()
{
var game = new VariableCaptureGame();
int gameInput = 5;
game.Run(gameInput);
int jTry = 10;
bool result = game.isEqualToCapturedLocalVariable!(jTry);
Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");
int anotherJ = 3;
game.updateCapturedLocalVariable!(anotherJ);
bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);
Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
}
// Output:
// Local variable before lambda invocation: 0
// 10 is greater than 5: True
// Local variable after lambda invocation: 10
// Captured local variable is equal to 10: True
// 3 is greater than 5: False
// Another lambda observes a new value of captured variable: True
}
Le regole seguenti si applicano all'ambito della variabile nelle espressioni lambda:
- Una variabile acquisita non verrà sottoposta alla Garbage Collection fino a quando il delegato che vi fa riferimento non diventa idoneo alla raccolta dei rifiuti.
- Le variabili introdotte all'interno di un'espressione lambda non sono visibili nel metodo di inclusione.
- Un'espressione lambda non può acquisire direttamente un parametro in, refo out dal metodo di inclusione.
- Una istruzione return in un'espressione lambda non implica il ritorno del metodo di contenimento.
- Un'espressione lambda non può contenere un'istruzione goto, breako continue se la destinazione di quell'istruzione di salto è esterna al blocco dell'espressione lambda. È anche un errore avere un'istruzione di salto all'esterno del blocco di espressioni lambda se la destinazione è all'interno del blocco.
È possibile applicare il modificatore static
a un'espressione lambda per impedire l'acquisizione involontaria di variabili locali o dello stato dell'istanza tramite l'espressione lambda:
Func<double, double> square = static x => x * x;
Un'espressione lambda statica non può acquisire variabili locali o lo stato dell'istanza dagli ambiti circostanti, ma può fare riferimento a membri statici e definizioni costanti.
Specifica del linguaggio C#
Per ulteriori informazioni, consultare la sezione espressioni di funzione anonime della specifica del linguaggio C# .
Per altre informazioni su queste funzionalità, vedere le note sulla proposta di funzionalità seguenti: