Délégués et Événements en C#
Apprenez les délégués et événements en C# pour comprendre la programmation événementielle avec des exemples concrets.
En C#, les delegates et les events constituent la base de la programmation orientée événements.
Un delegate est une référence typée (type-safe) qui peut pointer vers une méthode.
Un event permet à une classe de notifier le monde extérieur qu’« une action spécifique s’est produite ».
Ces mécanismes permettent de construire des architectures faiblement couplées (loosely coupled)
et rendent possible l’exécution dynamique de différentes méthodes.
Qu’est-ce qu’un Delegate ?
Un delegate est un type qui peut contenir des références vers des méthodes ayant une signature spécifique (paramètres et type de retour). En d’autres termes, il permet de traiter les méthodes comme des variables.
Une variable delegate peut pointer vers une méthode compatible et l’invoquer dynamiquement. Cela permet d’écrire un code flexible et faiblement couplé.
// Définition d’un delegate
public delegate void Notify(string message);
class Program
{
static void SendEmail(string message)
{
Console.WriteLine("Email envoyé : " + message);
}
static void Main()
{
Notify notifyHandler;
// Associer une méthode au delegate
notifyHandler = SendEmail;
notifyHandler("La réunion est à 10h00.");
}
}
Dans cet exemple, le delegate notifyHandler
référence la méthode SendEmail et l’exécute.
Delegates Multicast
Un delegate multicast est un delegate capable de référencer plusieurs méthodes simultanément.
En C#, les delegates deviennent multicast par défaut lorsqu’ils sont combinés avec l’opérateur +=.
Lorsqu’un delegate multicast est invoqué, chaque méthode de la liste d’invocation est exécutée dans l’ordre où elle a été ajoutée. Cela est particulièrement utile pour diffuser une notification à plusieurs abonnés.
Exemple
using System;
class Program
{
delegate void Notify(string message);
static void Main()
{
Notify notifier = LogToConsole;
notifier += LogToFile;
notifier += SendEmail;
notifier("Build terminé.");
}
static void LogToConsole(string message)
{
Console.WriteLine("[Console] " + message);
}
static void LogToFile(string message)
{
Console.WriteLine("[Fichier] " + message);
}
static void SendEmail(string message)
{
Console.WriteLine("[Email] " + message);
}
}
Une méthode peut être supprimée de la liste d’invocation avec l’opérateur -= :
notifier -= SendEmail;
- Les delegates multicast sont utilisés en interne par les events.
- Si un gestionnaire lève une exception, l’exécution des suivants est interrompue.
- Si le delegate possède un type de retour, seul le résultat de la dernière méthode est retourné.
Comportement du type de retour
Si un delegate multicast possède un type de retour, toutes les méthodes de la liste d’invocation sont exécutées, mais seul le résultat de la dernière méthode est retourné.
using System;
class Program
{
delegate int Calculate(int x);
static void Main()
{
Calculate calc = Square;
calc += Double;
int result = calc(5);
Console.WriteLine(result);
// Résultat : 10 (résultat de la dernière méthode : Double)
}
static int Square(int x)
{
Console.WriteLine("Square appelée");
return x * x;
}
static int Double(int x)
{
Console.WriteLine("Double appelée");
return x * 2;
}
}
Les méthodes Square et Double sont toutes deux exécutées ;
cependant, le delegate retourne uniquement le résultat de la dernière méthode ajoutée.
- Toutes les méthodes sont exécutées dans l’ordre.
- Seule la valeur de retour de la dernière méthode est conservée.
- Pour cette raison, les delegates multicast sont généralement utilisés avec un type de retour
void(comme les events).
Qu’est-ce qu’un Event ?
Un event est un mécanisme qui signale qu’une action s’est produite.
Les events sont construits sur des delegates et fournissent un accès contrôlé.
Le mot-clé event est utilisé pour la déclaration,
et il ne peut être accessible de l’extérieur qu’avec += (abonnement)
ou -= (désabonnement).
Cela garantit que :
- L’event ne peut être déclenché qu’à l’intérieur de la classe qui le déclare.
- Le code externe ne peut pas remplacer la liste d’invocation.
- L’encapsulation est préservée.
public class Button
{
// Définition de l’event (EventHandler est un delegate standard)
public event EventHandler? Click;
public void SimulateClick()
{
Console.WriteLine("Bouton cliqué !");
// Déclenchement de l’event
Click?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
var btn = new Button();
// Abonnement à l’event
btn.Click += (s, e) => Console.WriteLine("Event : le bouton a été cliqué.");
btn.SimulateClick();
}
}
L’opérateur null-conditionnel (?.) garantit que l’event
n’est invoqué que s’il existe des abonnés,
évitant ainsi une NullReferenceException.
Dans cet exemple, la classe Button publie un event Click.
L’expression lambda abonnée est exécutée lorsque l’event est déclenché.
Ce modèle est largement utilisé dans des frameworks UI tels que WinForms et WPF.
EventHandler et EventArgs
En C#, le delegate standard le plus couramment utilisé pour les events
est EventHandler.
Il suit le modèle classique :
(object sender, EventArgs e).
En dérivant de EventArgs,
il est possible de transmettre des données supplémentaires liées à l’event.
Cette approche respecte les directives officielles de conception des events en .NET.
public class OrderEventArgs : EventArgs
{
public int OrderId { get; }
public decimal Amount { get; }
public OrderEventArgs(int orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}
}
public class OrderService
{
public event EventHandler<OrderEventArgs>? OrderCreated;
public void CreateOrder(int id, decimal amount)
{
Console.WriteLine($"Commande créée (ID={id}, Montant={amount})");
// Déclenchement de l’event
OrderCreated?.Invoke(this, new OrderEventArgs(id, amount));
}
}
class Program
{
static void Main()
{
var service = new OrderService();
service.OrderCreated += (s, e) =>
{
Console.WriteLine($"Notification : commande reçue (#{e.OrderId}, {e.Amount} USD)");
};
service.CreateOrder(101, 250m);
}
}
Dans cet exemple, la classe OrderService déclenche l’event
OrderCreated lorsqu’une nouvelle commande est créée.
L’abonné reçoit les informations de la commande via la classe personnalisée
OrderEventArgs.
Cette approche garantit la sécurité de type et rend la communication orientée événements claire et structurée.
Delegate vs Event (Encapsulation)
Bien que les events soient construits sur des delegates, ils ne sont pas la même chose. La différence principale réside dans l’encapsulation et le contrôle d’accès.
Un champ delegate peut être modifié librement depuis l’extérieur de la classe, tandis qu’un event restreint l’accès externe et protège la liste d’invocation.
Exemple avec Delegate
public class Publisher
{
public Action? OnChange;
}
Comme OnChange est un champ delegate :
- Le code externe peut l’assigner avec
=. - Tous les abonnés existants peuvent être supprimés accidentellement.
- Il peut être défini à
null. - Il peut être invoqué directement depuis l’extérieur.
publisher.OnChange = null; // Supprime tous les abonnés
publisher.OnChange = SomeMethod; // Remplace la liste d’invocation
publisher.OnChange?.Invoke(); // Peut être déclenché depuis l’extérieur
Exemple avec Event
public class Publisher
{
public event Action? OnChange;
}
Comme OnChange est déclaré comme un event :
- Le code externe ne peut utiliser que
+=et-=. - Il ne peut pas écraser la liste d’invocation avec
=. - Il ne peut pas invoquer l’event directement.
- L’event ne peut être déclenché qu’à l’intérieur de la classe qui le déclare.
- Les delegates offrent de la flexibilité.
- Les events offrent un accès contrôlé et préservent l’encapsulation.
- Pour les notifications publiques, les events doivent presque toujours être préférés aux champs delegate.
Alternatives modernes : Action & Func
En C# moderne, définir des types delegate personnalisés est souvent inutile.
À la place, vous pouvez utiliser les delegates génériques intégrés
Action et Func.
Ces delegates sont flexibles, concis et largement utilisés dans l’écosystème .NET (y compris LINQ, la programmation asynchrone et les API basées sur les tâches).
Action
Action représente une méthode qui ne retourne aucune valeur.
Elle peut accepter zéro ou plusieurs paramètres.
Action log = message => Console.WriteLine(message);
log("Application started.");
Func
Func<T, TResult> représente une méthode qui retourne une valeur.
Le dernier paramètre générique représente toujours le type de retour.
Func add = (a, b) => a + b;
int result = add(5, 3); // 8
Actionest utilisé lorsqu’aucune valeur de retour n’est nécessaire.Funcest utilisé lorsqu’une valeur de retour est requise.- L’utilisation de ces delegates intégrés réduit le code répétitif.
- La plupart des projets C# modernes préfèrent
ActionetFuncaux delegates personnalisés.
Désabonnement & Fuites de mémoire
Un aspect important du travail avec les events est le désabonnement. Si vous vous abonnez à un event sans jamais vous désabonner, cela peut entraîner des fuites de mémoire inattendues.
Les events maintiennent une référence forte vers leurs abonnés. Tant que l’objet publisher est vivant, le subscriber ne peut pas être collecté par le garbage collector.
Pourquoi est-ce un problème ?
Imaginez un publisher à longue durée de vie (par exemple, un service ou un singleton) et un subscriber à courte durée de vie (par exemple, un composant UI). Si le subscriber ne se désabonne pas, il restera en mémoire même s’il n’est plus nécessaire.
public class Publisher
{
public event Action? OnChange;
public void Raise()
{
OnChange?.Invoke();
}
}
public class Subscriber
{
private readonly Publisher _publisher;
public Subscriber(Publisher publisher)
{
_publisher = publisher;
_publisher.OnChange += HandleChange;
}
private void HandleChange()
{
Console.WriteLine("Change detected.");
}
public void Dispose()
{
// Important : se désabonner !
_publisher.OnChange -= HandleChange;
}
}
Si Dispose n’est jamais appelé,
l’instance Subscriber reste référencée par l’event
et ne peut pas être collectée par le garbage collector.
- Désabonnez-vous toujours des events lorsque la durée de vie du subscriber se termine.
- Cela est particulièrement important dans les frameworks UI (WinForms, WPF, Blazor, etc.).
- Publisher à longue durée de vie + subscriber à courte durée de vie = fuite de mémoire potentielle.
- Implémenter
IDisposableest une solution courante pour un désabonnement sécurisé.
Sécurité des threads & Bonnes pratiques
Lors de l’utilisation des events dans des environnements multithread,
la sécurité des threads devient importante.
Un problème courant survient lors de la vérification de null
avant d’invoquer un event.
Condition de course potentielle
if (OnChange != null)
{
OnChange(this, EventArgs.Empty);
}
Entre la vérification null et l’invocation,
un autre thread pourrait se désabonner de l’event.
Cela peut provoquer une NullReferenceException.
Modèle d’invocation sécurisé
Pour éviter cette condition de course, copiez la référence du delegate dans une variable locale avant l’invocation :
var handler = OnChange;
handler?.Invoke(this, EventArgs.Empty);
Cela garantit que la liste d’invocation ne peut pas changer entre la vérification et l’appel de la méthode.
Syntaxe moderne
Dans la plupart des cas, l’opérateur null-conditionnel offre une manière concise et sûre d’invoquer les events :
OnChange?.Invoke(this, EventArgs.Empty);
- Utilisez des modèles d’invocation sécurisés dans les environnements multithread.
- Privilégiez
?.Invokepour une syntaxe moderne et claire. - Les events sont des delegates multicast — un gestionnaire défaillant peut arrêter l’exécution.
- En cas de besoin de haute fiabilité, isolez les appels avec try/catch.
Quand utiliser Delegates et Events ?
-
Delegates :
À utiliser pour passer des méthodes en paramètres,
implémenter des callbacks ou créer des flux d’exécution flexibles.
En C# moderne,
ActionetFuncsont généralement préférés. - Events : À utiliser pour exposer des notifications depuis une classe tout en préservant l’encapsulation. Idéal pour les interactions UI, les événements métier (domain events) et les changements d’état.
- Bonne pratique : Utilisez des events pour les notifications publiques et évitez d’exposer directement des champs delegate. Désabonnez-vous lorsque nécessaire et appliquez des modèles d’invocation sécurisés.
TL;DR
delegate: Références typées vers des méthodes avec support multicast.event: Delegate contrôlé permettant l’abonnement tout en préservant l’encapsulation.EventHandleretEventArgs: Suivent le modèle standard des events en .NET pour transmettre des données.ActionetFunc: Alternatives modernes aux delegates personnalisés.- Désabonnez-vous des events lorsque nécessaire pour éviter les fuites de mémoire.
- Utilisez les events pour les notifications et les delegates (ou
Action/Func) pour les callbacks et le flux d’exécution.
Articles connexes
Classes, Objets, Propriétés et Méthodes en C#
Découvrez comment les classes, objets, propriétés et méthodes en C# constituent les fondements de la programmation orientée objet.
Expressions Lambda en C#
Apprenez les expressions lambda en C#, avec une syntaxe concise, Func et Action, et des exemples pratiques avec LINQ.
Interfaces et Classes Abstraites en C#
Découvrez les interfaces et classes abstraites en C#, leurs différences et quand les utiliser pour concevoir un code maintenable.
Méthodes et utilisation des paramètres en C#
Apprenez à définir des méthodes et à utiliser des paramètres en C#, y compris les paramètres par valeur et par référence avec exemples.