Func et Action sont des types délégués génériques que .NET déclare pour vous, ce qui permet de stocker et de passer des fonctions sans déclarer d'abord un type délégué. Func renvoie une valeur ; Action ne renvoie rien.
Sortie :
4
46.00
False
Hello!
Mia scored 87
La règle à retenir : dans un Func, le dernier argument de type est le type de retour. Func<int, int, bool> prend deux int et renvoie un bool. Func<int> ne prend rien et renvoie un int. Les arguments de type d'une Action sont tous des paramètres.
La famille
| Type | Paramètres | Renvoie |
|---|---|---|
Action | aucun | void |
Action<T> | T | void |
Action<T1, T2> ... jusqu'à 16 | T1, T2, ... | void |
Func<TResult> | aucun | TResult |
Func<T, TResult> | T | TResult |
Func<T1, T2, TResult> ... jusqu'à 16 | T1, T2, ... | TResult |
Predicate<T> | T | bool |
Comparison<T> | T, T | int (négatif, zéro, positif) |
Converter<TIn, TOut> | TIn | TOut |
Predicate, Comparison et Converter sont plus anciens et apparaissent dans les méthodes de List<T> et Array : FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). LINQ utilise Func partout, c'est pourquoi list.Where(...) et list.FindAll(...) prennent des lambdas de même forme mais de types différents :
Sortie :
17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y
Passer des fonctions à des méthodes
Un paramètre Func ou Action permet à l'appelant de fournir la partie d'un algorithme qui varie. Deux formes courantes sont « exécute ceci et mesure-le » et « essaie ceci jusqu'à ce que ça marche » :
Sortie :
attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2
Retry est générique, donc son Func<int, T> peut renvoyer n'importe quoi ; le compilateur a déduit T comme string à partir de la lambda. La lambda passée à Twice modifie la variable locale clicks, ce qui fonctionne parce qu'une lambda capture la variable elle-même, pas une copie de sa valeur.
Groupes de méthodes : passer une méthode par son nom
Vous n'avez pas besoin d'une lambda pour remplir un Func ou une Action. Le nom d'une méthode existante dont la signature correspond (un « groupe de méthodes ») se convertit directement :
Sortie :
parsed: 43
sat is weekend: True
total quantity: 20
Select(int.Parse) équivaut à Select(s => int.Parse(s)), avec un niveau d'appel en moins. Le compilateur choisit la surcharge qui correspond au type cible : int.Parse a plusieurs surcharges, et seule Parse(string) correspond à Func<string, int>. Quand le compilateur ne peut pas trancher (plusieurs surcharges conviennent aussi bien, ou les arguments de type d'une méthode générique ne peuvent pas être déduits d'un groupe de méthodes), l'appel ne compile pas, et une lambda qui rend l'appel explicite corrige le problème.
Un groupe de méthodes utilisé comme Func s'exécute quand même plus tard, chaque fois que le délégué est invoqué, exactement comme une lambda. La différence tient seulement à l'écriture.
Renvoyer des fonctions
Une méthode peut construire et renvoyer une fonction. Le Func renvoyé emporte les valeurs à partir desquelles il a été construit :
Sortie :
140
180
162
Twice(staff) construit une nouvelle fonction qui applique deux fois la remise du personnel : 200, puis 180, puis 162.
Un dictionnaire de fonctions
Stocker des fonctions dans un dictionnaire transforme un long switch en table de correspondance. C'est la forme habituelle des gestionnaires de commandes, des calculatrices et des actions de menu, et de nouvelles entrées peuvent être ajoutées à l'exécution :
Sortie :
12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE
Ajouter un opérateur revient à ajouter une entrée au dictionnaire, sans rien changer à la boucle. Le StringComparer.OrdinalIgnoreCase passé au second dictionnaire rend les noms de commandes insensibles à la casse.
Fonctions async : Func<Task>, pas Action
Avec du code async, le type délégué décide si l'appelant peut attendre la fin du travail :
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
Une lambda async affectée à une Action devient une méthode async void : rien ne peut l'attendre, et une exception à l'intérieur n'atteint jamais l'appelant. Elle est relevée sur le pool de threads (ou sur le thread d'interface dans une application de bureau), où elle fait en général planter le processus. Quand vous écrivez une méthode qui accepte du travail async, prenez un Func<Task> (ou Func<Task<T>>).
Quand déclarer son propre délégué
Func et Action couvrent la plupart des besoins. Déclarez un type délégué nommé quand :
- La signature a des paramètres
ref,outouin.Funcne peut pas les exprimer, donc une fonction à la manière deTryParsedemandedelegate bool TryParser<T>(string text, out T value);. - Le nom documente un rôle dans une API publique.
delegate bool Validator(string input)dans une signature de méthode en dit plus queFunc<string, bool>, et ses noms de paramètres apparaissent dans IntelliSense. - Vous avez besoin d'un paramètre
params, que les délégués génériques ne peuvent pas avoir non plus.
Les événements sont l'autre endroit où les types délégués personnalisés apparaissent encore, même si le EventHandler<TEventArgs> intégré couvre la plupart des cas. La page sur les délégués explique comment déclarer et combiner des types délégués.
Questions fréquentes
Quelle est la différence entre Func et Action en C# ?
Func renvoie une valeur et Action non. Dans Func<int, string, bool>, le dernier argument de type (bool) est le type de retour et les autres sont des paramètres. Action<int, string> prend un int et une string et renvoie void. Les deux existent en versions de 0 à 16 paramètres.
Comment passer une fonction en paramètre en C# ?
Déclarez le paramètre comme un Func ou une Action de la bonne forme et appelez-le dans la méthode : static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. Les appelants passent une lambda (Apply(80m, p => p * 0.9m)) ou un nom de méthode (Apply(80m, HalfPrice)).
Qu'est-ce que Predicate<T> en C# ?
Un type délégué qui prend un T et renvoie un bool, de même forme que Func<T, bool>. Il est antérieur à Func et est utilisé par List<T>.Find, FindAll, RemoveAll, Exists et Array.Find. Les deux types ne se convertissent pas l'un en l'autre, mais une lambda se convertit vers l'un comme vers l'autre.
Comment utiliser Func avec du code asynchrone ?
Utilisez Func<Task> pour une fonction async sans résultat et Func<T, Task<TResult>> pour une fonction avec résultat, afin que l'appelant puisse l'attendre avec await. Évitez d'affecter une lambda async à une Action : elle devient async void, qu'on ne peut pas attendre et dont l'appelant ne peut pas intercepter les exceptions.
Func peut-il avoir des paramètres out ou ref ?
Non. Les paramètres génériques de Func et Action sont des paramètres par valeur ordinaires, il n'y a donc aucun moyen d'écrire Func<string, out int, bool>. Pour une signature comme TryParse, déclarez un délégué personnalisé : delegate bool TryParser<T>(string text, out T value);.