Chargement...

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 :


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 :


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 :

  • 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
  • Action est utilisé lorsqu’aucune valeur de retour n’est nécessaire.
  • Func est 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 Action et Func aux 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 IDisposable est 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 ?.Invoke pour 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 ?


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.
  • EventHandler et EventArgs : Suivent le modèle standard des events en .NET pour transmettre des données.
  • Action et Func : 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