Les delegate ça fait vite des nœuds au cerveau, mais je vais tâcher d'expliquer ça de façon simple. Le plus aisé, je pense, et sans rentrer dans les détails techniques, est de dire que le mot clé delegate permet de créer un type de fonction, de la même manière que le mot class permet de créer un type d'objet. Par exemple si je veux définir une classe qui s'appelle Product et possède des propiétés Id et Name :
1 2 3 4 5 6
|
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
} |
Si je veux définir un type de fonction qui s'appelle OperationSurDesInt, prend en paramètre deux int et renvoie un int :
public delegate int OperationSurDesInts(int a, int b);
On va pouvoir utiliser ce delegate avec des méthodes qui respectent sa signature.
public static int Max(int a, int b) { return a > b ? a : b; }
Ou :
public static int Compare(int a, int b) { return a - b; }
Une fois qu'on a défini un type de méthode on peut le manipuler comme un type d'objet. On peut en faire un champ ou une propriété :
1 2 3 4 5 6 7 8 9 10
| public class Class1
{
private OperationSurDesInts _operation;
public OperationSurDesInts Operation
{
get { return _operation; }
set { _operation = value; }
}
} |
On peut l'instancier :
_operation = new OperationSurDesInts(Max);
Ou plus simplement :
On peut également assigner une lambda expression, c.à.d une fonction improvisée :
_operation = (a, b) => a > b ? a : b;
Les delegate sont ensuite utiliser pour la conception d'événements (en fait une implémentation du patter Observateur / Observable). Un événement (event) est une sorte de collection qui permet de stocker des méthodes respectant une certaine signature et de les appeler au besoin. Pour une événement de réception de message :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| namespace Samples
{
public class Message { public string Content { get; set; } }
public delegate void ProcessMessage(Message message);
public class Messages
{
public event ProcessMessage NewMessage;
protected void SignalMessage(Message message)
{
NewMessage?.Invoke(message);
}
public void Send(Message message)
{
if (message != null) SignalMessage(message);
}
}
} |
Par convention les événements des classes du framework correspondent à une signature qui ne renvoie pas de valeur et prend en paramètre 1 un objet (sender) et en paramètre 2 une class dérivée de EventArgs. Ainsi le delegate de l'événement standard :
public delegate void EventHandler(object sender, EventArgs e);
Le delegate PropertyChanged :
public delegate void PropertyChanged(object sender, PropertyChangedEventArgs e);
Il y a un ensemble de convention sur les noms des événements et de leurs attributs mais je ne les ai pas toutes en tête pour le moment.
Notons qu'afin de ne pas créer des delegate à tire-larigot le framework met à disposition des delegate générique : Action pour les méthodes n'ayant pas de valeur de retour et Func<TResult> pour celle qui ont une valeur de retour ; elle peuvent prendre jusqu'à une quinzaine de paramètres d'entrée (mais personne ne crée de fait de méthode avec 15 paramètre d'entrée, n'est-ce-pas ?) ; pour Func<TResult> la valeur de retour est portée sur le dernier paramètre.
public Action<int> Execute { get; set; }
public Func<double, double, int> Comparer { get; set; }
Ainsi ta fonction d'événement pourra être transmise à ce type de méthode :
public void RegisterCallback(Func<long, long, long, long, long> callback) { }
Mais si tu l'utilises souvent tu voudras sans doute faire un delegate dedié :
1 2 3
| public delegate long EventFunc(long arg, long evclass, long evtype, long evarg);
public void RegisterCallback(EventFunc callback) { } |
Enfin pour ceux qui n'ont pas encore perdu toute leur santé mentale, un mot sur (si je me souviens bien de la terminologie) les paramètre contravariant et les retours covariants. Un delegate peux accepter une méthode dont les paramètres d'entrée sont plus permissifs que ceux qu'il fournit (ex : object pour n'importe-quoi d'autre) et dont le type de retour est plus restreint que celui attendu (ex : List pour IEnumerable). L'exemple en code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| namespace Samples
{
public abstract class Animal
{
public string Name { get; set; }
}
public class Human : Animal { }
public class Frog : Animal { }
public delegate Animal HumanMetamorphosis(Human target);
public static class MyWorldOfMagic
{
public static HumanMetamorphosis CurrentMetamorphosis { get; set; }
public static Frog EvilSpell(Animal victim)
{
return victim != null ? new Frog { Name = victim.Name } : null;
}
public static void EvilIsFun()
{
CurrentMetamorphosis = EvilSpell;
}
public static void NightOfTheWitch()
{
var witch = new Witch();
CurrentMetamorphosis = witch.WitchCurse;
}
}
public class Witch
{
public Frog WitchCurse(Human victim)
{
return MyWorldOfMagic.EvilSpell(victim);
}
}
} |
EvilSpell peut être affecté à CurrentMetamorphosis qui est de type HumanMetamorphosis. Le delegate peut passer un Human à la méthode qui attend un Animal ; tandis que l'objet Frog renvoyé au delegate pourra ensuite être renvoyé par le delegate qui expose le type Animal.
EDIT : en fait j'ai écrit un gros pavé, s'il tombe dans une mare il va faire de sacrés éclaboussures ; en tout cas félicitation a ceux qui l'auront lu en entier
Partager