Menu

Func et Action en C# : délégués génériques, Predicate et exemples

Comment fonctionnent Func et Action en C# : Func<T, TResult> pour les fonctions qui renvoient une valeur, Action<T> pour celles qui n'en renvoient pas, Predicate et Comparison, passer et renvoyer des fonctions, un dictionnaire de commandes, les lambdas async, et quand déclarer plutôt son propre délégué.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

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

TypeParamètresRenvoie
Actionaucunvoid
Action<T>Tvoid
Action<T1, T2> ... jusqu'à 16T1, T2, ...void
Func<TResult>aucunTResult
Func<T, TResult>TTResult
Func<T1, T2, TResult> ... jusqu'à 16T1, T2, ...TResult
Predicate<T>Tbool
Comparison<T>T, Tint (négatif, zéro, positif)
Converter<TIn, TOut>TInTOut

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, out ou in. Func ne peut pas les exprimer, donc une fonction à la manière de TryParse demande delegate 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 que Func<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);.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER