Condividi tramite


Espressioni lambda e funzioni anonime

Usare un'espressione lambda per creare una funzione anonima. Usare l'operatore di dichiarazione lambda => per separare l'elenco di parametri dell'espressione lambda dal corpo. Un'espressione lambda può essere di una delle due forme seguenti:

Per creare un'espressione lambda, è necessario specificare gli eventuali parametri di input a sinistra dell'operatore lambda e un'espressione o un blocco di istruzioni sull'altro lato.

Qualsiasi espressione lambda può essere convertita in 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 alcun valore, può essere convertita in uno dei tipi delegati Action, altrimenti può essere convertita in uno dei tipi delegati Func. Ad esempio, un'espressione lambda che include due parametri e non restituisce alcun valore può essere convertita in delegato Action<T1,T2>. Un'espressione lambda che include un parametro e restituisce un valore può essere convertita in delegato Func<T,TResult>. Nell’esempio seguente, l’espressione lambda x => x * x, che specifica un parametro chiamato x e restituisce il valore di x quadrato, è assegnato a una variabile di un tipo delegato:

Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25

Un'espressione lambda può essere convertita anche in tipo albero delle espressioni, come mostrato 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 passare 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 albero delle espressioni Expression<Func<TSource,TResult>>. In entrambi i casi è possibile usare la stessa espressione lambda per specificare il valore del parametro. Questo approccio fa sì che le due chiamate Select risultino simili anche se in realtà il tipo degli oggetti creati dalle espressioni lambda è diverso.

Espressioni lambda

Un'espressione lambda con un'espressione a destra dell'operatore => è denominata espressione lambda. Un'espressione lambda dell'espressione restituisce il risultato dell'espressione e ha il formato di base seguente:

(input-parameters) => expression

Il corpo di un'espressione lambda può essere costituito da una chiamata al metodo. Se tuttavia si creano alberi delle espressioni valutati al di fuori del contesto di Common Language Runtime (CLR) di .NET Framework, ad esempio in SQL Server, non è consigliabile usare chiamate al metodo nelle espressioni lambda. I metodi non avranno alcun significato all'esterno del contesto di .NET Common Language Runtime (CLR).

Espressioni lambda dell'istruzione

Un'espressione lambda dell'istruzione è simile a un'espressione lambda con la differenza che le istruzioni sono racchiuse tra parentesi graffe:

(input-parameters) => { <sequence-of-statements> }

Il corpo di un'espressione lambda dell'istruzione può essere costituito da un numero qualsiasi di istruzioni, sebbene in pratica generalmente non ce ne siano più di due o tre.

Action<string> greet = name =>
{
    string greeting = $"Hello {name}!";
    Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!

Non è possibile usare espressioni lambda di istruzione per creare alberi delle espressioni.

Parametri di input di un'espressione lambda

I parametri di input di un'espressione lambda sono racchiusi tra parentesi. Specificare zero parametri di input con 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 parametri di input devono essere tutti di tipo esplicito o tutti di tipo implicito. In caso contrario, si verifica un errore del compilatore CS0748.

È possibile usare i discard per specificare due o più parametri di input di un'espressione lambda che non vengono 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 dell'evento.

Nota

Per garantire 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 matrici params 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 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

Usando le parole chiave async e await è facile creare istruzioni ed espressioni lambda che includono l'elaborazione asincrona. Nell'esempio seguente di Windows Form è presente 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 utilizzando un'espressione lambda asincrona. Per aggiungere il gestore, aggiungere un modificatore async prima dell'elenco di parametri lambda, come illustrato di seguito:

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 altre informazioni su come creare e usare i metodi asincroni, vedere Programmazione asincrona con async e await.

Espressioni lambda e tuple

Il linguaggio C# offre il supporto predefinito per le tuple. È possibile specificare una tupla come argomento di un'espressione lambda e l'espressione lambda può restituire una tupla. In alcuni casi, il compilatore C# usa l'inferenza del tipo per determinare i tipi di componenti della tupla.

Per definire una tupla, è necessario racchiudere tra parentesi un elenco di componenti delimitato da virgole. L'esempio riportato sotto usa una tupla con tre componenti per passare una sequenza di numeri a un'espressione lambda, la quale raddoppia ogni valore e restituisce una tupla con tre componenti che contiene 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. È possibile tuttavia definire una tupla usando i componenti denominati, come illustra l'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#, consultare Tipi di tupla.

Espressioni lambda con operatori query standard

LINQ to Objects, tra altre implementazioni, ha un parametro di input il cui tipo appartiene alla famiglia Func<TResult> di delegati generici. Questi delegati usano parametri di tipo per definire il numero e il tipo di parametri di input e il tipo restituito del delegato. I delegatiFunc sono utili per incapsulare le espressioni definite dall'utente applicate a ogni elemento in un set di dati di origine. Considerare ad esempio il tipo delegato Func<T,TResult>:

public delegate TResult Func<in T, out TResult>(T arg)

È possibile creare un'istanza Func<int, bool> del delegato, dove int è un parametro di input e bool è il valore restituito. Il valore restituito è sempre specificato nell'ultimo parametro di tipo. Func<int, string, bool>, ad esempio, definisce un delegato con due parametri di input, int e string, e un tipo restituito bool. Il seguente delegato Func, 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 specificare un'espressione lambda quando il tipo di argomento è Expression<TDelegate>, ad esempio negli operatori query standard definiti nel tipo Queryable. Quando si specifica un argomento Expression<TDelegate>, l'espressione lambda viene compilata per un albero delle espressioni.

L'esempio seguente usa l'operatore 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 è in grado di dedurre il tipo del parametro di input oppure è possibile specificarlo in modo esplicito. Questa espressione lambda particolare conta i numeri interi (n) che divisi per due danno il resto di 1.

L'esempio seguente crea una sequenza contenente tutti gli elementi presenti nella matrice numbers che si trovano a sinistra di 9, vale a dire 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

In questo esempio viene illustrato come specificare più parametri di input racchiudendoli tra parentesi. Il metodo restituisce tutti gli elementi presenti nella matrice numbers finché non viene rilevato un numero il cui valore è inferiore alla relativa posizione all'interno della 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 al metodo nelle 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 del tipo nelle espressioni lambda

Quando si scrivono espressioni lambda, spesso non occorre specificare un tipo per i parametri di input, perché il compilatore può dedurlo in base al corpo dell'espressione lambda, ai tipi di parametro e ad altri fattori, come descritto nelle specifiche del linguaggio C#. Per la maggior parte degli operatori di query standard, il primo input è il tipo degli elementi nella sequenza di origine. Pertanto, se si esegue una query su un oggetto IEnumerable<Customer>, si deduce che la variabile di input sia un oggetto Customer, ovvero che si dispone dell'accesso ai relativi metodi e proprietà:

customers.Where(c => c.City == "London");

Di seguito sono riportate le regole generali per l'inferenza del tipo nelle espressioni lambda:

  • 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 del delegato corrispondente.
  • Il valore restituito dell'espressione lambda, se presente, deve essere convertibile in modo implicito nel tipo restituito 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". In alcuni casi, tuttavia, può essere utile fare riferimento in modo informale al "tipo" di un'espressione lambda. Tale "tipo" informale fa riferimento al tipo delegato o tipo Expression in cui viene convertita l'espressione lambda.

A partire da C# 10, 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 seguente dichiarazione:

var parse = (string s) => int.Parse(s);

Il compilatore può dedurre che parse sia Func<string, int>. Il compilatore sceglie un delegato Func o Action disponibile, se ne esiste uno appropriato. In caso contrario, sintetizza un tipo delegato. Ad esempio, il tipo delegato viene sintetizzato se l'espressione lambda contiene 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 overload hanno 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.LambdaExpression o System.Linq.Expressions.Expression e 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. Considerare 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 restituito esplicito

In genere, il tipo restituito di un'espressione lambda è ovvio e dedotto. Per alcune espressioni questo non funziona:

var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type

A partire da C# 10, è 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

A partire da C# 10, è 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 reflection. Una conseguenza di questa decisione è che System.Diagnostics.ConditionalAttribute non si può applicare a un'espressione lambda.

Acquisire variabili esterne e ambito delle variabili nelle espressioni lambda

Le espressioni lambda possono fare riferimento a variabili esterne. Queste variabili esterne sono le variabili incluse nell'ambito del metodo che definisce l'espressione lambda oppure nell'ambito del tipo che contiene l'espressione lambda. Le variabili acquisite in questo modo vengono archiviate per poter essere utilizzate nell'espressione lambda anche se le variabili diventano esterne all'ambito e vengono sottoposte a Garbage Collection. Una variabile esterna deve essere assegnata prima di poter essere utilizzata in un'espressione lambda. Nell'esempio seguente vengono illustrate 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 delle variabili nelle espressioni lambda:

  • Una variabile acquisita non sarà sottoposta a Garbage Collection finché il delegato a cui fa riferimento non diventa idoneo per il Garbage Collection.
  • Le variabili introdotte in un'espressione lambda non sono visibili nel metodo contenitore.
  • Un'espressione lambda non può acquisire direttamente un parametro in, ref o out dal metodo contenitore.
  • Un'istruzione return in un'espressione lambda non causa la restituzione del metodo contenitore.
  • Un'espressione lambda non può contenere un'istruzione goto, break o continue se la destinazione di tale istruzione di salto è esterna al blocco dell'espressione lambda. È anche errato inserire all'esterno del blocco dell'espressione lambda un'istruzione di salto se la destinazione è interna al blocco.

È possibile applicare il modificatore static a un'espressione lambda per impedire l'acquisizione involontaria delle 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 uno stato dell’istanza dall’inclusione di ambiti ma può fare riferimento a membri statici e definizioni costanti.

Specifiche del linguaggio C#

Per altre informazioni, vedere la sezione Espressioni di funzioni anonime della specifica del linguaggio C#.

Per ulteriori informazioni su queste funzionalità, consultare le note sulla proposta di funzionalità seguenti:

Vedi anche