65 votes

Comment désinscrire correctement un gestionnaire d'événements

Dans une revue de code, je suis tombé sur ceci (simplifié) fragment de code pour annuler l'inscription d'un gestionnaire d'événement:

 Fire -= new MyDelegate(OnFire);

Je pensais que cela n'a pas d'annuler l'inscription du gestionnaire d'événements, car il crée un nouveau délégué qui n'avait jamais été enregistré auparavant. Mais la recherche de 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(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, le suivant s'est produit:

  1. Fire("Hello 1"); produit deux messages, comme prévu.
  2. Fire("Hello 2"); produit un message!
    Cela m'a convaincu que l'annulation de l'inscription new des délégués des travaux!
  3. Fire("Hello 3"); jeta un NullReferenceException.
    Débogage du code ont 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 de déléguer, le compilateur génère beaucoup de code derrière la scène. Mais je ne comprends toujours pas pourquoi mon raisonnement est faux.

Ce qui me manque?

Question supplémentaire: le fait qu' 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é, un chèque à l'encontre null est requis.

83voto

Bradley Grainger Points 12126

Le compilateur C# par défaut de mise en œuvre de l'ajout d'un gestionnaire d'événement appels Delegate.Combine, tandis que la suppression d'un gestionnaire d'événement appels Delegate.Remove:

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

Le Cadre de la mise en œuvre de l' Delegate.Remove n'a pas l'air à l' MyDelegate objet lui-même, mais à la méthode le délégué fait référence (Program.OnFire). Ainsi, il est parfaitement sûr pour créer un nouveau MyDelegate de l'objet lors de la désinscription d'un gestionnaire d'événements existante. De ce fait, le compilateur C# permet d'utiliser une abréviation de la syntaxe (qui génère exactement le même code dans les coulisses) lors de l'ajout/suppression des gestionnaires d'événements: vous pouvez omettre l' new MyDelegate partie:

Fire += OnFire;
Fire -= OnFire;

Lors de la dernière délégué est supprimé à partir du gestionnaire d'événement, Delegate.Remove renvoie la valeur null. Comme vous l'avez trouvé, il est essentiel de vérifier l'événement par rapport à null avant de le soulever:

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

Il est attribué à un temporaire variable locale pour se défendre contre une possible situation de compétition avec vous désabonner des gestionnaires d'événements sur d'autres threads. (Voir mon blog pour plus de détails sur le fil de sécurité de l'attribution du gestionnaire d'événement à une variable locale.) Une autre façon de se défendre contre ce problème est de créer un vide délégué qui est toujours souscrit; tandis que celui-ci utilise un peu plus de mémoire, le gestionnaire d'événement ne peut jamais être nul (et le code peut être plus simple):

public static event MyDelegate Fire = delegate { };

15voto

user37325 Points 171

Vous devriez toujours vérifier si un délégué n'a pas d'objectifs (sa valeur est null) avant de le tirer. Comme dit avant, une façon de le faire est de vous inscrire avec un anonyme méthode qui ne sera pas supprimé.

public event MyDelegate Fire = delegate {};

Cependant, c'est juste un hack pour éviter exception nullreferenceexceptions.

Tout simplement cheque si un délégué est null, avant de l'appeler n'est pas thread-safe comme un autre thread peut se désinscrire après le nul-vérifier et de le rendre nul, lors de l'invocation. Il existe une autre solution est de 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, d'éliminer la variable temporaire, et d'utiliser le délégué d'origine. (comme par Juval Lowy - Programmation .Composants NET)

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

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

Notez que sans l'attribut methodimpl(NoInlining) attribut le compilateur JIT pourrait inline la méthode la rendant inutile. Depuis les délégués sont immuables cette mise en œuvre est thread-safe. Vous pouvez utiliser cette méthode:

FireEvent(Fire,"Hello 3");

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X