Comment désenregistrer correctement un gestionnaire d’événement

Dans une revue de code, je suis tombé sur ce fragment de code (simplifié) pour désenregistrer un gestionnaire d’événement:

Fire -= new MyDelegate(OnFire); 

Je pensais que cela ne désinsère pas le gestionnaire d’événements car il crée un nouveau délégué qui n’avait jamais été enregistré auparavant. Mais en recherchant MSDN, j’ai trouvé plusieurs exemples de code qui utilisent cet idiome.

J’ai donc commencé une expérience:

 internal class Program { public delegate void MyDelegate(ssortingng msg); public static event MyDelegate Fire; private static void Main(ssortingng[] args) { Fire += new MyDelegate(OnFire); Fire += new MyDelegate(OnFire); Fire("Hello 1"); Fire -= new MyDelegate(OnFire); Fire("Hello 2"); Fire -= new MyDelegate(OnFire); Fire("Hello 3"); } private static void OnFire(ssortingng msg) { Console.WriteLine("OnFire: {0}", msg); } } 

À ma grande surprise, les événements suivants se sont produits:

  1. Fire("Hello 1"); produit deux messages, comme prévu.
  2. Fire("Hello 2"); produit un message!
    Cela m’a convaincu que le désinscription de new delegates fonctionne!
  3. Fire("Hello 3"); a lancé une NullReferenceException .
    Le débogage du code a montré que Fire est null après l’annulation de l’enregistrement de l’événement.

Je sais que pour les gestionnaires d’événements et les delegates, le compilateur génère beaucoup de code derrière la scène. Mais je ne comprends toujours pas pourquoi mon raisonnement est faux.

Qu’est-ce que je rate?

Question supplémentaire: du fait que Fire est null quand il n’y a pas d’événements enregistrés, je conclus que partout où un événement est déclenché, une vérification par rapport à null est requirejse.

L’implémentation par défaut du compilateur C # pour l’ajout d’un gestionnaire d’événements appelle Delegate.Combine , tout en supprimant les appels de gestionnaire d’événements Delegate.Remove :

 Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire)); 

L’implémentation de Delegate.Remove Framework ne regarde pas l’object MyDelegate lui-même, mais la méthode à laquelle se réfère le délégué ( Program.OnFire ). Ainsi, il est parfaitement sûr de créer un nouvel object MyDelegate lors du désabonnement d’un gestionnaire d’événement existant. De ce fait, le compilateur C # vous permet d’utiliser une syntaxe abrégée (qui génère exactement le même code en coulisse) lors de l’ajout / suppression de gestionnaires d’événements: vous pouvez omettre la new MyDelegate partie new MyDelegate :

 Fire += OnFire; Fire -= OnFire; 

Lorsque le dernier délégué est supprimé du gestionnaire d’événements, Delegate.Remove renvoie null. Comme vous l’avez découvert, il est essentiel de vérifier l’événement contre null avant de le déclencher:

 MyDelegate handler = Fire; if (handler != null) handler("Hello 3"); 

Il est assigné à une variable locale temporaire pour se défendre contre une éventuelle condition de concurrence avec le désabonnement des gestionnaires d’événements sur d’autres threads. (Voir l’article sur mon blog pour plus de détails sur la sécurité des threads concernant l’atsortingbution du gestionnaire d’événements à une variable locale.) Une autre façon de se défendre contre ce problème consiste à créer un délégué vide toujours abonné; Bien que cela utilise un peu plus de mémoire, le gestionnaire d’événements ne peut jamais être nul (et le code peut être plus simple):

 public static event MyDelegate Fire = delegate { }; 

Vous devez toujours vérifier si un délégué n’a pas de cible (sa valeur est nulle) avant de le lancer. Comme nous l’avons déjà dit, une façon de faire est de s’abonner à une méthode anonyme qui ne sera pas supprimée.

 public event MyDelegate Fire = delegate {}; 

Cependant, ce n’est qu’un hack pour éviter les NullReferenceExceptions.

Il suffit de regarder si un délégué est nul avant d’appeler n’est pas threadsafe car un autre thread peut se désinscrire après la vérification de null et le rendre nul lors de l’appel. Une autre solution consiste à copier le délégué dans une variable temporaire:

 public event MyDelegate Fire; public void FireEvent(ssortingng msg) { MyDelegate temp = Fire; if (temp != null) temp(msg); } 

Malheureusement, le compilateur JIT peut optimiser le code, éliminer la variable temporaire et utiliser le délégué d’origine. (selon Juval Lowy – Programmation de composants .NET)

Donc, pour éviter ce problème, vous pouvez utiliser la méthode qui accepte un délégué comme paramètre:

 [MethodImpl(MethodImplOptions.NoInlining)] public void FireEvent(MyDelegate fire, ssortingng msg) { if (fire != null) fire(msg); } 

Notez que sans l’atsortingbut MethodImpl (NoInlining), le compilateur JIT pourrait intégrer la méthode qui le rend inutile. Comme les delegates sont immuables, cette implémentation est threadsafe. Vous pouvez utiliser cette méthode comme:

 FireEvent(Fire,"Hello 3");