Espressioni lambda, delegati ed eventi

Tip

Novità dello sviluppo di software? Iniziare prima con le esercitazioni introduttive . Prima di usare espressioni lambda, è necessario sviluppare competenze di base sui tipi e i metodi.

A volte si vuole passare una piccola parte di comportamento, una funzione, direttamente a un altro metodo. Ad esempio, è possibile filtrare un elenco, ma la condizione di filtro cambia a seconda della situazione. Anziché scrivere un metodo denominato separato per ogni condizione possibile, passare la condizione stessa come argomento.

Le espressioni lambda sono la funzionalità C# che rende possibile questa operazione. Un'espressione lambda è una funzione compatta e inline scritta senza assegnargli un nome. Si utilizza l'operatore freccia => con per separare l'elenco di parametri dal corpo:

x => x * 2

Lettura da sinistra a destra: x è il parametro di input, => indica "va a", e x * 2 è il corpo. Calcola il valore restituito. Quando non è presente alcun parametro o ne sono presenti più di uno, avvolgerli tra parentesi: () => 42 o (left, right) => left + right.

I delegati supportano espressioni lambda

Per usare un'espressione lambda, il compilatore C# deve conoscere due aspetti: i tipi dei parametri e il tipo restituito. Tale descrizione, i tipi di parametro più il tipo restituito, viene chiamato tipo delegato.

Un tipo delegato è un tipo che rappresenta una firma del metodo. Una variabile di un tipo delegato può contenere qualsiasi metodo corrispondente, ad esempio un'espressione lambda o un metodo denominato, purché i tipi di parametro e la corrispondenza del tipo restituito corrispondano.

Si dichiara un tipo delegato con la delegate parola chiave :

delegate int Transform(int value);

Questa dichiarazione dice: "Transform è un tipo delegato per i metodi che accettano uno int e restituiscono ". int È quindi possibile assegnare un'espressione lambda o un metodo denominato a una variabile di tale tipo:

Transform doubler = x => x * 2;    // assign a lambda expression
Transform squarer = Square;         // assign a named method

Console.WriteLine(doubler(5));      // 10
Console.WriteLine(squarer(5));      // 25

static int Square(int value) => value * value;

Sia doubler che squarer contengono un valore di tipo Transform. Li chiami esattamente come i metodi normali. Il compilatore verifica che qualsiasi elemento assegnato corrisponda alla firma dichiarata.

Tipi delegati predefiniti: Func e Action

La dichiarazione di un tipo delegato personalizzato per ogni situazione può essere ripetitiva. .NET fornisce due famiglie di tipi delegati generici, e , che coprono la maggior parte degli scenari, quindi raramente è necessario usare la parola chiave />

Entrambe le famiglie sono disponibili in versioni con da zero a sedici parametri di tipo di input, quindi si adattano a qualsiasi numero di input. La differenza principale tra le due famiglie è:

  • System.Func<T,TResult> (e Func<T1, T2, TResult>così via) rappresenta un metodo che restituisce un valore. L'ultimo parametro di tipo è sempre il tipo restituito; tutti quelli precedenti sono tipi di input.
  • System.Action<T> (e Action<T1, T2>così via) rappresenta un metodo che non restituisce nulla (void). Tutti i parametri di tipo sono tipi di input. System.Action senza parametri di tipo rappresenta un metodo senza input e nessun valore restituito.

Ad esempio, Func<int, int, int> descrive un metodo con due int input e un int risultato. Action<string> descrive un metodo con un string input e nessun valore restituito.

Func<int, int, int> add = (left, right) => left + right;
Action<string> report = message => Console.WriteLine($"Report: {message}");

int total = add(5, 9);
report($"5 + 9 = {total}");

Usare nomi di parametri descrittivi nelle espressioni lambda in modo che i lettori possano comprendere la finalità senza analizzare il corpo completo del metodo.

Passare un'espressione lambda a un metodo

Quando un metodo dichiara un Func parametro o Action , i chiamanti passano un'espressione lambda che corrisponde a tale tipo delegato. Il compilatore verifica che i tipi di parametro lambda e il tipo restituito corrispondano al tipo delegato dichiarato. Se non corrispondono, il codice non viene compilato.

int[] numbers = [1, 2, 3, 4, 5, 6];
int[] evenNumbers = Filter(numbers, value => value % 2 == 0).ToArray();

Console.WriteLine(string.Join(", ", evenNumbers));

Il Filter metodo dichiara un Func<int, bool> parametro denominato predicate. Il tipo Func<int, bool> indica agli utenti la forma prevista: uno int input, uno bool risultato. Il chiamante passa value => value % 2 == 0 come argomento. Questo modello viene visualizzato in LINQ e in molte API di .NET.

Mantenere autonome le espressioni lambda

Un'espressione lambda può fare riferimento a variabili dal codice circostante. L'acquisizione significa che la lambda mantiene un riferimento a una variabile dichiarata al di fuori del proprio corpo. La combinazione dell'espressione lambda e delle variabili acquisite è detta chiusura.

Quando non è necessario acquisire nulla, aggiungere il static modificatore all'espressione lambda. Una funzione lambda statica può usare solo i propri parametri e i valori dichiarati all'interno del proprio blocco di codice. Non può acquisire le variabili locali o lo stato dell'istanza dall'ambito circostante.

Func<int, bool> isEven = static value => value % 2 == 0;

Console.WriteLine(isEven(14));
Console.WriteLine(isEven(15));

Le espressioni lambda statiche rendono chiaro l'intento e impediscono catture accidentali.

Usare i parametri discard quando gli input sono irrilevanti

In alcuni casi una firma del delegato include parametri non necessari. Usare il valore discard _ per segnalare esplicitamente tale scelta.

Gli esempi comuni includono gestori eventi in cui non si usano sender o EventArgs, i callback in cui sono necessari solo alcuni input e overload LINQ che forniscono un indice non usato.

Action<int, int, string> statusUpdate = (_, _, message) => Console.WriteLine(message);
statusUpdate(200, 42, "Operation completed");

Gli scarti migliorano la leggibilità perché mostrano quali parametri sono importanti.

Gli eventi forniscono notifiche facoltative

Un evento è un meccanismo usato da un oggetto (autore) per notificare ad altri oggetti ( sottoscrittori) quando si verifica un evento. L'editore non deve sapere chi è in ascolto o quanti sottoscrittori sono presenti. I sottoscrittori scelgono di partecipare.

Gli eventi sono basati su delegati. Un evento è un campo delegato con restrizioni aggiuntive applicate dal event parola chiave: il codice esterno può solo iscriversi (+=) o disiscriversi (-=) dall'evento. Solo la classe che dichiara l'evento può richiamarlo (generarlo).

La convenzione di .NET per i tipi di delegato di eventi è System.EventHandler<TEventArgs>, dove T è il tipo di dati inclusi nella notifica. La firma ha sempre due parametri: l'oggetto sender (l'oggetto che ha generato l'evento) e i dati dell'evento di tipo T.

MessagePublisher publisher = new();
publisher.MessagePublished += (_, message) => Console.WriteLine($"Received: {message}");

publisher.Publish("Records updated");

Scorrere il codice:

  • MessagePublisher dichiara event EventHandler<string>? MessagePublished. La event parola chiave indica che i chiamanti possono solo sottoscrivere o annullare la sottoscrizione. Non possono richiamarlo direttamente.
  • publisher.MessagePublished += (_, message) => ... si sottoscrive con un'espressione lambda. Scarta _ il parametro sender perché questo gestore non ne ha bisogno.
  • publisher.Publish("Records updated") genera l'evento ed esegue ogni gestore registrato.

La sottoscrizione è facoltativa. Nel metodo ?.Invoke(...), Publish significa che l'evento scatta solo quando è associato almeno un sottoscrittore. L'editore attiva l'evento senza sapere o preoccuparsi se qualcuno è in ascolto.

Vedere anche