CodeGym /Cours /C# SELF /Erreurs typiques avec les delegates et les events

Erreurs typiques avec les delegates et les events

C# SELF
Niveau 54, Leçon 0
Disponible

1. Introduction

Travailler avec les delegates et les events en C# est agréable et pratique — le langage fait beaucoup pour vous. Cependant derrière cette façade se cachent pas mal de pièges : fuites de mémoire invisibles, bugs étranges dus à une double subscription, désynchronisation des handlers et même des exceptions soudaines pendant la diffusion des notifications. Les delegates et les events offrent de puissantes possibilités, mais exigent qu'on gère attentivement le cycle de vie des objets, qu'on comprenne les threads et qu'on sache exactement comment fonctionnent les appels et les désabonnements. Si vos events fonctionnent "presque toujours", mais parfois ne se déclenchent pas ou provoquent des erreurs étranges — vous n'êtes pas seul ! Voyons où les développeurs, même expérimentés, se plantent le plus souvent et comment l'éviter.

Tableau des erreurs principales

Erreur Conséquence Comment éviter
Double subscription Le handler est appelé plusieurs fois Surveiller la subscription, enlever avant d'ajouter
Pas de désabonnement (fuite mémoire) Les subscribers restent en mémoire, « zombies » Désabonner toujours, utiliser IDisposable
Exception dans un handler Les autres handlers ne seront pas appelés try/catch dans les handlers ou itérer manuellement
Modification des subscribers pendant l'event Omission ou duplication d'appels Itérer sur une copie de la liste des handlers (GetInvocationList())
Appel de l'event quand MyEvent == null NullReferenceException Vérifier null, utiliser ?.Invoke
Signature du handler ne correspond pas Erreur de compilation Vérifier la signature
Appeler l'event depuis l'extérieur Erreur de compilation Appeler seulement via OnEventName
Events static mal placés Mélange des subscriptions Ne pas rendre static sans vraie nécessité
Problèmes de closure avec les lambdas Valeurs inattendues Faire une copie locale de la variable

2. Multi-subscription et appels multiples

Nature de l'erreur

Si vous abonnez plusieurs fois le même handler au même event, chaque opération += ajoute votre méthode à la file du delegate. En conséquence, le handler sera appelé autant de fois qu'il a été ajouté.

Comment ça se manifeste ?

Imaginez que vous avez un bouton et un handler pour le click :

Button btn = new Button();
btn.Click += OnButtonClick; // On s'abonne
btn.Click += OnButtonClick; // Et re-abonnement !

Maintenant à chaque click la méthode OnButtonClick sera appelée deux fois. Si dans le handler vous incrémentez un compteur ou écrivez dans un log, vous verrez des résultats doublés.

Comment corriger ?

Habituellement la double subscription arrive à cause d'une mauvaise structure du code — par exemple si le += est dans une méthode appelée plusieurs fois (différents cycles de vie d'une form).

  • Veillez à l'endroit où la subscription a lieu.
  • Ne placez pas le += dans des étapes du cycle de vie qui peuvent être ré-exécutées.
  • Parfois utile d'utiliser une subscription "unique" — enlever le handler avant de l'ajouter :
myEvent -= MyHandler; // On enlève au cas où
myEvent += MyHandler; // Puis on ré-ajoute

C'est sûr : si le handler n'existait pas, le -= ne fera rien.

3. Subscriber zombie

Nature de l'erreur

Si un subscriber s'abonne à un publisher qui vit longtemps et qu'il ne se désabonne pas, le garbage collector ne pourra pas le collecter : le publisher garde une référence au delegate du handler, donc à tout l'objet subscriber. Résultat — fuite mémoire.

Exemple typique

public class TemporaryPopup : IDisposable
{
    private Window _hostWindow;
    public TemporaryPopup(Window window)
    {
        _hostWindow = window;
        _hostWindow.Closed += OnHostClosed;
    }

    private void OnHostClosed(object sender, EventArgs e)
    {
        // ...
    }

    public void Dispose()
    {
        _hostWindow.Closed -= OnHostClosed; // N'oubliez pas de vous désabonner !
    }
}

Si on oublie d'appeler Dispose() ou de le gérer, même en supprimant toutes les références vers TemporaryPopup, l'objet ne sera pas détruit — la fenêtre garde la référence à son handler.

Comment éviter ?

  • Implémentez IDisposable dans les subscribers si leur cycle de vie est plus court que celui du publisher.
  • Utilisez le pattern using ou appelez explicitement Dispose() :
using (var popup = new TemporaryPopup(mainWindow))
{
    // ...
} // Ici Dispose sera appelé automatiquement

Dans les apps GUI — désabonnez-vous lors de la fermeture de la fenêtre/form (par exemple dans les handlers de fermeture ou dans Dispose() de la form).

4. Gestion des exceptions dans les handlers

Nature de l'erreur

Quand un event appelle des dizaines de handlers et qu'un seul lance une exception, les autres handlers ne seront pas exécutés.

Démo

public event EventHandler MyEvent;

public void Raise()
{
    MyEvent?.Invoke(this, EventArgs.Empty);
}

Si l'un des méthodes abonnées à MyEvent jette une exception, les autres ne seront pas appelées — la chaîne est interrompue.

Comment gérer ça ?

  • Dans les handlers d'events, gérez les exceptions localement (try/catch) ou laissez-les consciemment remonter.
  • Dans des scénarios complexes — itérez manuellement les subscribers et isolez chaque erreur :
var handlers = MyEvent?.GetInvocationList();
foreach (var handler in handlers)
{
    try
    {
        handler.DynamicInvoke(this, EventArgs.Empty);
    }
    catch (Exception ex)
    {
        // Logging, récupération d'urgence
    }
}

5. Modifier la liste des subscribers pendant la diffusion

Nature de l'erreur

Si un handler lors de l'event se désabonne lui-même ou d'autres, cela peut perturber l'ordre d'appel : certains handlers seront sautés ou appelés en double.

Comment éviter ?

  • Ne modifiez pas les subscriptions depuis les handlers.
  • Si nécessaire — itérez sur une copie de la liste de delegates :
var handlers = MyEvent?.GetInvocationList();
foreach (EventHandler handler in handlers)
{
    handler(this, EventArgs.Empty);
}

6. Confusion avec la valeur null du delegate (pas d'abonnés)

Nature de l'erreur

Si personne n'est abonné à l'event, son delegate vaut null. L'appel sans vérification entraîne un NullReferenceException.

Exemple (mauvais)

public event EventHandler MyEvent;

public void Raise()
{
    MyEvent(this, EventArgs.Empty); // Si pas d'abonnés — exception !
}

Comment faire correctement ?

  • Utiliser l'appel sécurisé : MyEvent?.Invoke(this, EventArgs.Empty).
  • Ou appliquer la technique classique thread-safe : copier le delegate dans une variable locale et l'appeler.

7. Mélange de delegates/events avec des signatures différentes

Nature de l'erreur

Les delegates sont fortement typés. Une discordance entre la signature du method-handler et la signature du delegate de l'event provoque une erreur de compilation.

Exemple

public event EventHandler<string> TextChanged;

void WrongHandler(object sender, int number) { /* ... */ }

TextChanged += WrongHandler; // Erreur de compilation !

Utilisez les delegates standards EventHandler et EventHandler<T>, et assurez-vous que les signatures correspondent exactement.

8. Tentative d'appeler l'event en dehors du publisher

Nature de l'erreur

event est un delegate encapsulé : le code externe ne peut pas l'invoquer directement (seulement ajouter/enlever des handlers).

Exemple

public class MyPublisher
{
    public event EventHandler SomethingHappened;
}

var publisher = new MyPublisher();
publisher.SomethingHappened?.Invoke(publisher, EventArgs.Empty); // Erreur !

Comment faire correctement ?

L'appel doit se faire via une méthode protégée/publique du publisher — habituellement OnEventName. De l'extérieur — seulement +=/-=.

9. Erreurs avec les accessors add et remove

Nature de l'erreur

Les accessors personnalisés pour les events permettent de contrôler la subscription, mais on peut facilement briser l'ordre d'appel ou la sécurité vis-à-vis des threads.

Exemple

public event EventHandler MyEvent
{
    add { /* ... */ }
    remove { /* ... */ }
}

Si vous n'êtes pas sûr — utilisez l'implémentation standard des events. Si vous faites du custom, tenez la doc à portée de main et pensez à la synchronisation.

10. Accès aux events depuis des contextes static et instance

Nature de l'erreur

On peut accidentellement déclarer un event static alors qu'il devrait appartenir à l'instance. Dans ce cas toutes les instances partagent la même file de subscribers.

Exemple

public static event EventHandler GlobalEvent; // Oups!
// Les instances perdent leur individualité, les subscriptions se mélangent

Comment éviter ?

Rendez un event static seulement si vous avez besoin d'un scope global (par exemple un log global). Dans les autres cas — encapsulez au niveau de l'instance.

11. Problèmes de capture de variables dans les lambdas

Nature de l'erreur

Les lambdas capturent les variables par référence. Dans les boucles cela mène souvent à la « dernière valeur ».

Exemple

for (int i = 0; i < 5; i++)
{
    button.Click += (s, e) => Console.WriteLine(i);
}
// Tous les handlers afficheront "5"

Comment faire correctement ?

for (int i = 0; i < 5; i++)
{
    int copy = i; // Copie locale
    button.Click += (s, e) => Console.WriteLine(copy);
}

12. Mélange entre références faibles et fortes : Advanced “Weak Events”

Dans les grosses applications (par exemple WPF) on utilise le mécanisme des « weak events », où le publisher garde une WeakReference vers le subscriber pour ne pas empêcher le garbage collector. Les weak events aident contre les fuites, mais le subscriber peut être collecté et ne plus recevoir l'event.

Détails : Weak Event Patterns (MSDN)

13. Absence de standard pour le naming et les signatures des events

Nature de l'erreur

Respectez les signatures et noms standards : les events — au passé (Changed, Closed, Completed), les arguments — des descendants de EventArgs.

Exemple « incorrect » :

public delegate void SomethingHappens(int what);
// ...
public event SomethingHappens Something;

Exemple « correct » :

public event EventHandler<EventArgs> SomethingHappened;

Pour vos propres events, utilisez presque toujours EventHandler ou EventHandler<T>. Vos collègues (et votre futur vous) vous remercieront.

2
Mission
C# SELF, niveau 54, leçon 0
Bloqué
Gestion des exceptions dans les gestionnaires d'événements
Gestion des exceptions dans les gestionnaires d'événements
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION