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.
GO TO FULL VERSION