Un délégué est un type qui représente une signature de méthode. Une variable d'un type délégué contient une référence vers une méthode de cette signature, et appeler la variable appelle la méthode. C'est ainsi que C# traite les fonctions comme des valeurs : vous pouvez les stocker, les passer à d'autres méthodes et les appeler plus tard.
Sortie :
40
70
68.00
delegate decimal PriceRule(decimal price); déclare un nouveau type, de la même façon que class. Toute méthode qui prend un decimal et renvoie un decimal lui correspond. La variable rule a pointé tour à tour vers trois méthodes différentes, et chaque appel a exécuté celle qu'elle contenait.
Déclarer, créer et invoquer
Une déclaration de type délégué ressemble à une signature de méthode précédée du mot-clé delegate. Elle se place en général au niveau du namespace, à côté des classes, ou imbriquée dans une classe.
Il existe quatre façons de créer une instance de délégué :
PriceRule a = HalfPrice; // method group (most common)
PriceRule b = new PriceRule(HalfPrice); // explicit constructor, same thing
PriceRule c = p => p - 5m; // lambda expression
PriceRule d = delegate (decimal p) { return p; }; // anonymous method (C# 2 syntax)
Et deux façons de l'appeler, qui sont identiques : rule(80m) et rule.Invoke(80m). Le compilateur vérifie la signature au moment de l'affectation, donc une méthode incompatible est une erreur de compilation plutôt qu'une surprise à l'exécution.
Un délégué peut aussi pointer vers une méthode d'instance. Il retient alors à la fois la méthode et l'objet sur lequel l'appeler :
Sortie :
Ana got: Your order shipped
target: Mailbox, method: Receive
Target est l'objet sur lequel le délégué appellera la méthode (null pour une méthode statique), et Method décrit la méthode. Retenir la cible signifie aussi qu'un délégué garde cet objet en vie, ce qui compte pour les événements.
Les délégués comme callbacks
Le rôle principal d'un délégué est de permettre à une méthode d'appeler du code choisi par son appelant. La méthode définit quand quelque chose se produit ; l'appelant définit quoi :
Sortie :
[## ] 25%
[##### ] 50%
[####### ] 75%
[##########] 100%
import finished
ImportRows ne sait rien des consoles ni des barres. La même méthode peut piloter une barre de progression, une ligne de log ou une mise à jour d'interface, selon ce que l'appelant passe. List.Sort(Comparison<T>), Array.Find(Predicate<T>), Task.Run(Action) et chaque opérateur LINQ suivent ce schéma.
Délégués multicast : += et -=
Un délégué peut contenir plus d'une méthode. += ajoute une méthode à sa liste d'invocation, -= la retire, et invoquer le délégué appelle chaque méthode dans l'ordre :
Sortie :
email sent for A-1001
stock updated for A-1001
warehouse notified for A-1001
3 handlers
email sent for A-1002
warehouse notified for A-1002
Trois détails des délégués multicast comptent en pratique :
- Valeurs de retour : si le type délégué renvoie une valeur, invoquer un délégué multicast ne renvoie que le résultat de la dernière méthode. Pour recueillir tous les résultats, parcourez
GetInvocationList()et invoquez chaque délégué vous-même. - Exceptions : si une méthode lève une exception, les méthodes restantes ne s'exécutent pas et l'exception remonte à l'appelant.
- Immuabilité : les délégués sont immuables.
+=crée un nouveau délégué avec une liste plus longue et le réaffecte à la variable, c'est pourquoi retirer avec-=sur une copie n'affecte pas l'original.
Retirer une lambda avec -= ne fonctionne que si vous passez la même instance de délégué. Écrire deux fois la même lambda crée deux délégués différents, donc handlers -= id => Log(id); ne retire rien. Stockez la lambda dans une variable quand vous comptez la désabonner.
Délégués null et ?.Invoke
Une variable délégué à laquelle rien n'est affecté vaut null, et retirer la dernière méthode avec -= laisse aussi null. Invoquer null lève NullReferenceException :
Sortie :
no handler, no crash
saved draft.txt
NullReferenceException
?.Invoke est la façon standard d'appeler un callback facultatif. L'opérateur ?. ne peut pas se placer directement devant les parenthèses (onSaved?("x") n'est pas une syntaxe valide), donc le schéma appelle par son nom la méthode Invoke du délégué, qui fait la même chose que l'appel abrégé.
Délégués, Func, Action et événements
Déclarer un type délégué pour chaque signature serait fastidieux, donc .NET en fournit des génériques :
| Type intégré | Déclaration personnalisée équivalente |
|---|---|
Action | delegate void Action(); |
Action<T> | delegate void Action<T>(T arg); |
Func<TResult> | delegate TResult Func<TResult>(); |
Func<T, TResult> | delegate TResult Func<T, TResult>(T arg); |
Predicate<T> | delegate bool Predicate<T>(T obj); |
Le PriceRule du premier exemple a la même forme que Func<decimal, decimal>, et pourtant ce sont deux types distincts qui ne se convertissent pas l'un en l'autre. Même deux types intégrés de signatures identiques ne le font pas :
Func<int, bool> isPositive = x => x > 0;
Predicate<int> p = isPositive;
// error CS0029: Cannot implicitly convert type 'System.Func<int, bool>' to 'System.Predicate<int>'
Predicate<int> ok = new Predicate<int>(isPositive); // wrap it explicitly
Predicate<int> ok2 = x => isPositive(x); // or with a lambda
Une lambda se convertit vers n'importe quel type délégué compatible, donc les API prennent en général des lambdas et la question ne se pose jamais. La page Func et Action couvre la famille générique et les cas où une déclaration personnalisée vaut encore la peine.
Les événements reposent directement sur les délégués. Un champ event est un délégué multicast que le code extérieur peut seulement modifier avec += et -=, jamais invoquer ni écraser. La liste d'abonnés de l'exemple multicast ci-dessus est exactement ce qu'un événement gère pour vous, avec cette protection supplémentaire. Les événements ont leur propre page.
Questions fréquentes
Qu'est-ce qu'un délégué en C# ?
Un délégué est une référence typée vers une méthode. Vous déclarez un type délégué avec une signature, comme delegate decimal PriceRule(decimal price);, et toute méthode de cette signature (statique, d'instance ou lambda) peut être stockée dans une variable de ce type puis appelée plus tard à travers elle. Les délégués sont la façon dont C# passe des fonctions comme des valeurs.
Quelle est la différence entre un délégué et Func en C# ?
Func<T, TResult> et Action<T> sont des types délégués que .NET déclare déjà pour vous, génériques sur les types des paramètres et du retour. Une déclaration delegate personnalisée donne au type un nom et des noms de paramètres qui documentent son rôle, et prend en charge les paramètres ref, out et in. Pour la plupart du code, Func et Action suffisent.
Qu'est-ce qu'un délégué multicast en C# ?
Un délégué qui contient plusieurs méthodes. += ajoute une méthode à sa liste d'invocation et -= en retire une ; invoquer le délégué les appelle toutes dans l'ordre. Si le délégué renvoie une valeur, l'appelant ne reçoit que le résultat de la dernière méthode, et une exception dans une méthode empêche les suivantes de s'exécuter.
Comment invoquer sans risque un délégué qui peut être null ?
Utilisez l'opérateur conditionnel null : onProgress?.Invoke(50);. Une variable délégué à laquelle aucune méthode n'est affectée vaut null, et l'appeler directement lève NullReferenceException. ?.Invoke lit la variable une seule fois, c'est donc sûr aussi si un autre thread retire le dernier gestionnaire au même moment.
Quand déclarer son propre type délégué plutôt qu'utiliser Func ou Action ?
Quand la signature a besoin de paramètres ref, out ou in (Func et Action ne peuvent pas les exprimer), quand un type nommé rend une API publique plus claire (delegate bool Validator(string input)), ou quand vous voulez que les noms de paramètres apparaissent dans IntelliSense. Sinon, préférez Func et Action, que tout développeur .NET reconnaît.