1. Callback et programmation asynchrone
Callback (callback) — c'est un mécanisme pour passer une méthode qui doit être appelée à la fin d'une opération. Très souvent utilisé dans les opérations asynchrones, timers, traitement de données, interfaces utilisateur.
Exemple 1 : Opération asynchrone avec callback
Supposons qu'on ait une application où l'utilisateur saisit une requête, et le résultat arrive avec un délai (par exemple, depuis internet). Après réception des données on veut mettre à jour l'écran.
// Délégué pour le callback
public delegate void DataReceivedHandler(string result);
// Mécanisme de téléchargement de données de façon asynchrone (simulation)
public void DownloadDataAsync(DataReceivedHandler callback)
{
// Supposons que le téléchargement prend du temps (on simule avec un timer)
Task.Delay(1000).ContinueWith(_ =>
{
string data = "Résultats de la recherche : <données>";
callback(data); // Appel du délégué-callback
});
}
// Utilisation:
DownloadDataAsync(result =>
{
Console.WriteLine("Reçu : " + result);
});
Cette approche permet d'écrire du code très flexible où la logique après la réception du résultat est complètement séparée de la mécanique d'obtention des données.
2. Délégués comme paramètres de méthodes : stratégie et comparateurs
Tâche fréquente : permettre à l'utilisateur de passer la "logique" (fonction) dans votre méthode pour qu'il puisse définir comment comparer, filtrer ou transformer les éléments.
Exemple 2 : Implémentation du pattern Strategy via des délégués
Supposons qu'on ait un tri, mais on veut que l'utilisateur puisse trier différemment — par nom, par date, par taille, etc.
public delegate bool CompareFunc(int a, int b);
public void BubbleSort(int[] arr, CompareFunc compare)
{
for (int i = 0; i < arr.Length; i++)
{
for (int j = 0; j < arr.Length - 1; j++)
{
if (compare(arr[j], arr[j + 1]))
{
// Échange
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// Tri du plus grand au plus petit
CompareFunc descending = (a, b) => a < b;
// Utilisation
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // Affichera : 4, 3, 2, 1
Cette technique est un moyen universel d'injecter sa "stratégie" dans du code tiers sans modifier ses sources.
3. Méthodes anonymes, expressions lambda et délégués
Avec l'évolution de C#, il est peu pratique de déclarer une classe ou une méthode distincte pour chaque tâche. Heureusement, il y a les méthodes anonymes et les expressions lambda qui permettent de créer des délégués "à la volée".
Exemple 3 : Lambda comme délégué
Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15
Exemple 4 : Choix d'une opération par nom (switch + Delegates)
Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"
switch (userInput)
{
case "sum": op = (a, b) => a + b; break;
case "sub": op = (a, b) => a - b; break;
case "mul": op = (a, b) => a * b; break;
case "div": op = (a, b) => a / b; break;
default: throw new Exception("Opération inconnue !");
}
Console.WriteLine(op(6, 2));
N'oubliez pas la validation des données d'entrée — les délégués offrent flexibilité et lisibilité.
4. Délégués et chaînes de traitement ("chaine de responsabilités")
Puisque les délégués supportent la multi-adressabilité, ils permettent de construire facilement des chaînes de handlers.
Exemple 5 : Chaîne d'appel de filtres
Imaginons qu'on ait des "filtres" qui doivent traiter une chaîne.
public delegate string StringFilter(string input);
string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();
StringFilter filters = RemoveDigits;
filters += ToUpper;
// Le délégué appliquera la chaîne de filtres
string text = "Hello123";
foreach (StringFilter filter in filters.GetInvocationList())
{
text = filter(text);
}
Console.WriteLine(text); // Affichera : "PRIVET"
Important : si vous appelez simplement filters(text), la valeur retournée sera uniquement celle du dernier handler, pas celle de toute la chaîne ! Si vous avez besoin de "propagation" de la valeur, utilisez l'itération explicite via GetInvocationList() comme montré ci‑dessus.
5. Délégués pour le binding dynamique du comportement à la volée
Avant, pour remplacer un comportement par un autre il fallait créer des classes et des interfaces. Avec les délégués et les lambdas on peut exprimer une grande partie du "petit" polymorphisme par des fonctions.
Exemple 6 : Comportement d'un robot avec commande dynamique
public class Robot
{
public event Action<string>? OnCommandReceived;
public void ReceiveCommand(string command)
{
OnCommandReceived?.Invoke(command);
}
}
// Utilisation:
var robot = new Robot();
robot.OnCommandReceived += cmd => Console.WriteLine($"Le robot exécute : {cmd}");
robot.OnCommandReceived += cmd =>
{
if (cmd == "Allumer")
Console.WriteLine("Démarrage du système...");
};
// On teste
robot.ReceiveCommand("Allumer");
robot.ReceiveCommand("Avancer");
Cette technique est souvent utilisée dans les tests, prototypes, dans les conteneurs DI (dependency injection) et pour passer de la logique métier via des paramètres.
6. Délégués comme abonnements à l'état
Supposons qu'on ait une classe qui contient un certain état, et qu'on veuille notifier tous les abonnés quand il change. Grâce aux délégués (et aux événements) — c'est trivial.
Exemple 7 : Classe avec abonnement au changement
public class Notifier<T>
{
private T _value = default!;
public event Action<T>? ValueChanged;
public T Value
{
get => _value;
set
{
if (!Equals(_value, value))
{
_value = value;
ValueChanged?.Invoke(_value);
}
}
}
}
// Utilisation:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"Nouvelle valeur : {v}");
intValue.Value = 5; // Déclenche l'événement
intValue.Value = 10;
Cette approche est en quelque sorte du "reactive programming" minimal, la base pour MVVM, data binding et beaucoup de frameworks UI modernes.
7. Délégués, closures et portée lexicale
Les expressions lambda et les méthodes anonymes peuvent capturer des variables du contexte environnant (closure). C'est pratique, mais ça peut parfois causer des erreurs inattendues.
Exemple 8 : Capture de variable et "piège" de la boucle
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
foreach (var a in actions)
a(); // Affichera trois fois 3 (!)
Pourquoi ? Parce que la closure référence la même variable i, qui après la boucle vaut 3. Et si on veut mémoriser précisément les valeurs 0, 1, 2 ?
for (int i = 0; i < 3; i++)
{
int loopValue = i; // "gèle" la valeur courante
actions[i] = () => Console.WriteLine(loopValue);
}
Maintenant le code fonctionne comme prévu. Ces pièges sont parmi les erreurs les plus fréquentes chez les débutants utilisant les lambdas !
8. Combinaison des délégués
Les délégués multi-adressés (multicast) contiennent une liste de méthodes, et vous pouvez ajouter (+=) ou retirer (-=) des handlers.
Particularité : suppression par référence et signature
void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");
Action a = Handler1;
a += Handler2;
a -= Handler1; // Ne laissera que Handler2
a?.Invoke(); // Affichera "2"
Exemple : gestion dynamique des handlers
Action a = Handler1;
a += Handler1;
a -= Handler1; // Il reste maintenant UN seul Handler1 dans la liste !
9. Délégués, extensibilité et inversion de contrôle (IoC)
Dans les grosses applications il est souvent nécessaire que des composants puissent "appeler" du code externe tout en restant indépendants. Les délégués aident à intégrer des extensions, plugins et callbacks sans tight coupling.
Exemple : Injection de comportement via le constructeur
public class Greeter
{
private readonly Func<string> _getName;
public Greeter(Func<string> getName)
{
_getName = getName;
}
public void Greet() => Console.WriteLine($"Bonjour, {_getName()}!");
}
// Injection de comportements différents :
var greeter1 = new Greeter(() => "Anya");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());
greeter1.Greet(); // "Bonjour, Anya!"
greeter2.Greet(); // "Bonjour, 14:35!"
Dans la vraie vie cette technique est utilisée pour écrire du code testable et maintenable.
10. Nuances utiles
Délégués dans les interfaces standard et LINQ
Vous rencontrerez inévitablement des délégués si vous travaillez avec LINQ, les collections, l'asynchronisme.
- Beaucoup de méthodes comme List<T>.Find, Array.Sort, Where, Select acceptent des délégués (Func<T, bool>, Comparison<T>, etc.).
- Les méthodes LINQ permettent de passer la logique de filtrage, transformation, agrégation — sans créer de classes séparées.
Exemple : Comparateur pour trier des objets
var people = new[] { "Ivan", "Maria", "Petr" };
Array.Sort(people, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", people)); // Ivan, Petr, Maria
Délégués et currying (application partielle d'arguments)
Avec les méthodes anonymes / lambdas vous pouvez "fixer" une partie des paramètres et obtenir une nouvelle fonction.
Exemple : Application partielle
Func<int, int, int> sum = (x, y) => x + y;
// On crée une fonction qui ajoute toujours 10
Func<int, int> add10 = y => sum(10, y);
Console.WriteLine(add10(5)); // 15
Particularité de la comparaison des délégués
En C# les délégués peuvent être comparés pour l'égalité (==) s'ils ont la même liste d'appels (invocation list).
void Handler1() { }
void Handler2() { }
Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True
Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True
Mais si le délégué est construit à partir d'une méthode anonyme ou d'une lambda — ce sont les instances qui sont comparées.
Sérialisation des délégués
Les délégués peuvent être sérialisés, mais seulement si les méthodes auxquelles ils réfèrent sont définies dans des classes sérialisables et tous les types sont disponibles. Depuis .NET 8, le BinaryFormatter est désactivé par défaut et considéré obsolète, et dans les versions futures il sera complètement supprimé ; la sérialisation des délégués en production est pratiquement rarement utilisée.
Interaction entre délégués et événements : où mettre quoi ?
- Un délégué est un type/une variable qu'on peut invoquer explicitement.
- Un événement (event) est un moyen de restreindre l'accès à un délégué : de l'extérieur on peut seulement s'abonner/se désabonner (+=/-=), mais l'invocation n'est possible qu'à l'intérieur de la classe.
- Un événement est toujours de type délégué, mais tout délégué n'est pas un événement.
Comment choisir ? Si vous voulez que la logique puisse être définie "à l'extérieur" de la classe, utilisez des délégués. Si vous avez besoin de contrôler l'abonnement/désabonnement et protéger la variable — utilisez un événement.
11. Erreurs typiques lors du travail avec les délégués
Erreur n°1 : Confusion entre délégué et événement.
Utiliser un champ délégué public (public Action MyAction;) au lieu d'un événement (public event Action MyAction;) casse l'encapsulation. Le code externe peut accidentellement ou intentionnellement écraser tous les abonnés (instance.MyAction = null;) ou les appeler directement, ce qui viole la logique de la classe.
Erreur n°2 : Mauvaise gestion des valeurs de retour avec des délégués multicast.
Si un délégué retourne une valeur (par ex. Func<string, int>), lors d'un appel normal (myDelegate("test")) sera retourné uniquement le résultat du dernier méthode dans la chaîne. Pour obtenir les résultats de tous les abonnés, il faut itérer la liste d'appels via GetInvocationList().
// Exemple d'itération des résultats de tous les abonnés
var list = myDelegate.GetInvocationList();
foreach (var d in list)
{
var r = ((Func<string, int>)d)("test");
Console.WriteLine(r);
}
Erreur n°3 : Capture de la variable de boucle dans une closure.
Piège classique pour les débutants : une lambda créée à l'intérieur d'une boucle for capture la variable itérateur elle-même et non sa valeur courante.
// Incorrect : toutes les actions afficheront la dernière valeur i
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
// Correct : créer une copie locale pour chaque itération
for (int i = 0; i < 3; i++)
{
int copy = i;
actions[i] = () => Console.WriteLine(copy);
}
Erreur n°4 : Création de fuites de mémoire.
Si une méthode d'instance s'abonne à un délégué d'un objet de longue durée de vie mais ne se désabonne pas, cela crée une fuite mémoire. L'objet de longue durée maintient une référence vers l'abonné et le GC ne peut pas le collecter. Faites attention au désabonnement, surtout dans les classes au cycle de vie limité (par ex. composants UI).
GO TO FULL VERSION