2008-11-15 5 views
59

Dans une revue de code, je suis tombé sur ce fragment de code (simplifié) pour désenregistrer un gestionnaire d'événements:Comment désenregistrer correctement un gestionnaire d'événements

Fire -= new MyDelegate(OnFire); 

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

Alors j'ai commencé une expérience:

internal class Program 
{ 
    public delegate void MyDelegate(string msg); 
    public static event MyDelegate Fire; 

    private static void Main(string[] 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(string msg) 
    { 
     Console.WriteLine("OnFire: {0}", msg); 
    } 

} 

À ma grande surprise, ce qui suit est arrivé:

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

Je sais que pour les gestionnaires d'événements et les délégués, 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 qui me manque?

question supplémentaire: le fait que Fire est null quand il n'y a pas d'événements enregistrés, je conclus que partout un événement est déclenché, un chèque contre null est nécessaire.

Répondre

78

L'implémentation par défaut de compilateur C# d'ajouter un gestionnaire d'événements appelle Delegate.Combine, tout en retirant un gestionnaire d'événements appelle Delegate.Remove:

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

la mise en œuvre du cadre de Delegate.Remove ne regarde pas l'objet MyDelegate lui-même, mais à la méthode à laquelle le délégué fait référence (Program.OnFire). Ainsi, il est parfaitement sûr de créer un nouvel objet MyDelegate lors de la désinscription d'un gestionnaire d'événements existant. À cause de cela, le compilateur C# vous permet d'utiliser une syntaxe abrégée (qui génère exactement le même code dans les coulisses) lors de l'ajout/suppression de gestionnaires d'événements: vous pouvez omettre la new MyDelegate partie:

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 trouvé, il est essentiel de vérifier l'événement contre zéro avant de le soulever:

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

Il est assigné à une variable locale temporaire pour se défendre contre une condition de course possible avec des gestionnaires d'événements Désabonner sur d'autres sujets. (Voir my blog post pour plus de détails sur la sécurité des threads lors de l'affectation 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é; tandis que celui-ci utilise un peu plus de mémoire, le gestionnaire d'événements ne peut jamais être nulle (et le code peut être plus simple):

public static event MyDelegate Fire = delegate { }; 
+3

Yep. Pour compléter: cela me confondre pendant un certain temps aussi, d'autant plus que dans C# vous pouvez faire Fire + = new MyDelegate (OnFire) ou Fire + = OnFire; Ce dernier semble être plus simple, mais n'est que du sucre syntaxique pour le premier. –

+0

@Nicholas Piasecki: Merci; J'ai mis à jour ma réponse pour noter que (plutôt utile) sténographie. –

+0

Il peut être utile de noter que l'ajout d'un délégué de multidiffusion et sa désinscription ultérieure peuvent donner des résultats incorrects si l'une des méthodes de ce délégué de multidiffusion est également souscrite et désabonnée individuellement. – supercat

15

Vous devriez toujours vérifier si un délégué n'a pas de cible (sa valeur est nulle) avant sa cuisson . Comme dit précédemment, une façon de faire est de s'abonner avec une méthode anonyme qui ne sera pas supprimée.

public event MyDelegate Fire = delegate {}; 

Cependant, ceci est juste un hack pour éviter les exceptions NullReference. Il suffit de simplement indiquer si un délégué est nul avant d'invoquer n'est pas threadsafe car un autre thread peut être désinscrit après le null-check et le rendre nul en invoquant. Il y a une autre solution consiste à copier le délégué dans une variable temporaire:

public event MyDelegate Fire; 
public void FireEvent(string 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. (Comme par Juval Lowy - Programmation des 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, string msg) 
{ 
    if (fire != null) 
     fire(msg); 
} 

Notez que sans MethodImpl (NoInlining) attribuent la JIT compilateur pourrait inline la méthode le rendant sans valeur. Étant donné que les délégués sont immuables, cette implémentation est threadsafe. Vous pouvez utiliser cette méthode comme suit:

FireEvent(Fire,"Hello 3"); 
+0

Merci beaucoup d'avoir clarifié les problèmes de JIT. Pièges et pièges partout. – gyrolf

+7

En fait, ce n'est pas possible avec Microsoft CLR 2.0, en raison de son modèle de mémoire plus fort: http://code.logos.com/blog/2008/11/events_and_threads_part_4.html –

Questions connexes