Menu

Func e Action in C#: delegate generici, Predicate ed esempi

Come funzionano Func e Action in C#: Func<T, TResult> per le funzioni che restituiscono un valore, Action<T> per quelle che non lo fanno, Predicate e Comparison, passare e restituire funzioni, un dizionario di comandi, lambda async e quando dichiarare invece un delegate tuo.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Func e Action sono tipi delegate generici che .NET dichiara al posto tuo, così puoi memorizzare e passare funzioni senza dichiarare prima un tipo delegate. Func restituisce un valore; Action non restituisce niente.

Output:

4
46.00
False
Hello!
Mia scored 87

La regola da ricordare: in un Func, l'ultimo argomento di tipo è il tipo di ritorno. Func<int, int, bool> accetta due int e restituisce un bool. Func<int> non accetta niente e restituisce un int. Gli argomenti di tipo di un Action sono tutti parametri.

La famiglia

TipoParametriRestituisce
Actionnessunovoid
Action<T>Tvoid
Action<T1, T2> ... fino a 16T1, T2, ...void
Func<TResult>nessunoTResult
Func<T, TResult>TTResult
Func<T1, T2, TResult> ... fino a 16T1, T2, ...TResult
Predicate<T>Tbool
Comparison<T>T, Tint (negativo, zero, positivo)
Converter<TIn, TOut>TInTOut

Predicate, Comparison e Converter sono più vecchi e compaiono nei metodi di List<T> e Array: FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). LINQ usa Func ovunque, ed è per questo che list.Where(...) e list.FindAll(...) accettano lambda della stessa forma ma di tipi diversi:

Output:

17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y

Passare funzioni ai metodi

Un parametro Func o Action permette a chi chiama di fornire la parte di un algoritmo che cambia. Due forme comuni sono "esegui questo e misuralo" e "prova questo finché non funziona":

Output:

attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2

Retry è generico, quindi il suo Func<int, T> può restituire qualsiasi cosa; il compilatore ha dedotto T come string dalla lambda. La lambda passata a Twice modifica la variabile locale clicks, e funziona perché una lambda cattura la variabile stessa, non una copia del suo valore.

Method group: passare un metodo per nome

Non ti serve una lambda per riempire un Func o un Action. Il nome di un metodo esistente con una firma compatibile (un "method group") si converte direttamente:

Output:

parsed: 43
sat is weekend: True
total quantity: 20

Select(int.Parse) equivale a Select(s => int.Parse(s)), con un livello di chiamata in meno. Il compilatore sceglie l'overload adatto al tipo di destinazione: int.Parse ha diversi overload, e solo Parse(string) corrisponde a Func<string, int>. Quando il compilatore non riesce a sceglierne uno (più overload vanno bene allo stesso modo, o gli argomenti di tipo di un metodo generico non si possono dedurre da un method group), la chiamata non compila, e una lambda che rende esplicita la chiamata risolve il problema.

Un method group usato come Func viene comunque eseguito più tardi, ogni volta che il delegate viene invocato, esattamente come una lambda. La differenza sta solo in come si scrive.

Restituire funzioni

Un metodo può costruire e restituire una funzione. Il Func restituito porta con sé i valori da cui è stato costruito:

Output:

140
180
162

Twice(staff) costruisce una nuova funzione che applica due volte lo sconto per il personale: 200, poi 180, poi 162.

Un dizionario di funzioni

Memorizzare funzioni in un dizionario trasforma un lungo switch in una tabella di ricerca. È la forma abituale per gestori di comandi, calcolatrici e azioni di menu, e si possono aggiungere nuove voci a runtime:

Output:

12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE

Aggiungere un operatore significa aggiungere una voce al dizionario, senza toccare il ciclo. Lo StringComparer.OrdinalIgnoreCase passato al secondo dizionario rende i nomi dei comandi insensibili a maiuscole e minuscole.

Funzioni async: Func<Task>, non Action

Con il codice async, il tipo delegate decide se chi chiama può attendere il lavoro:

Func<Task> save = async () => await File.WriteAllTextAsync("a.txt", "data");
await save();                       // the caller can await it and see its exceptions

Func<int, Task<string>> load = async id => await FetchUserAsync(id);
string user = await load(42);

Action bad = async () => await File.WriteAllTextAsync("a.txt", "data");
bad();                              // async void: fire and forget, exceptions escape

Una lambda async assegnata a un Action diventa un metodo async void: niente può attenderla, e un'eccezione al suo interno non arriva mai a chi chiama. Viene rilanciata sul thread pool (o sul thread dell'interfaccia in un'app desktop), dove di solito fa terminare il processo. Quando scrivi un metodo che accetta lavoro asincrono, prendi un Func<Task> (o Func<Task<T>>).

Quando dichiarare un delegate tuo

Func e Action coprono la maggior parte delle esigenze. Dichiara un tipo delegate con nome quando:

  • La firma ha parametri ref, out o in. Func non può esprimerli, quindi una funzione in stile TryParse richiede delegate bool TryParser<T>(string text, out T value);.
  • Il nome documenta un ruolo in un'API pubblica. delegate bool Validator(string input) nella firma di un metodo dice più di Func<string, bool>, e i nomi dei suoi parametri compaiono in IntelliSense.
  • Ti serve un parametro params, che anche i delegate generici non possono avere.

Gli eventi sono l'altro posto in cui compaiono ancora tipi delegate personalizzati, anche se l'EventHandler<TEventArgs> integrato copre la maggior parte dei casi. La pagina sui delegate spiega come dichiarare e combinare i tipi delegate.

Domande frequenti

Che differenza c'è tra Func e Action in C#?

Func restituisce un valore e Action no. In Func<int, string, bool>, l'ultimo argomento di tipo (bool) è il tipo di ritorno e gli altri sono parametri. Action<int, string> accetta un int e una string e restituisce void. Entrambi esistono in versioni da 0 a 16 parametri.

Come passo una funzione come parametro in C#?

Dichiara il parametro come Func o Action della forma giusta e chiamalo dentro il metodo: static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. Chi chiama passa una lambda (Apply(80m, p => p * 0.9m)) o il nome di un metodo (Apply(80m, HalfPrice)).

Che cos'è Predicate<T> in C#?

Un tipo delegate che accetta un T e restituisce bool, la stessa forma di Func<T, bool>. È precedente a Func ed è usato da List<T>.Find, FindAll, RemoveAll, Exists e Array.Find. I due tipi non si convertono l'uno nell'altro, ma una lambda si converte in entrambi.

Come uso Func con il codice async?

Usa Func<Task> per una funzione async senza risultato e Func<T, Task<TResult>> per una con risultato, così chi chiama può fare await. Evita di assegnare una lambda async a un Action: diventa async void, che non si può attendere e le cui eccezioni non possono essere catturate da chi chiama.

Func può avere parametri out o ref?

No. I parametri generici di Func e Action sono normali parametri per valore, quindi non c'è modo di scrivere Func<string, out int, bool>. Per una firma come TryParse, dichiara un delegate personalizzato: delegate bool TryParser<T>(string text, out T value);.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA