Est-il nécessaire de supprimer explicitement les gestionnaires d'événements en C #


120

J'ai une classe qui propose quelques événements. Cette classe est déclarée globalement mais pas instanciée sur cette déclaration globale - elle est instanciée au besoin dans les méthodes qui en ont besoin.

Chaque fois que cette classe est nécessaire dans une méthode, elle est instanciée et les gestionnaires d'événements sont enregistrés. Est-il nécessaire de supprimer explicitement les gestionnaires d'événements avant que la méthode ne soit hors de portée?

Lorsque la méthode est hors de portée, l'instance de la classe en va de même. Le fait de laisser des gestionnaires d'événements enregistrés avec cette instance qui sort du champ d'application a-t-il une implication d'empreinte mémoire? (Je me demande si le gestionnaire d'événements empêche le GC de voir l'instance de classe comme n'étant plus référencée.)

Réponses:


184

Dans votre cas, tout va bien. C'est l'objet qui publie les événements qui maintient en direct les cibles des gestionnaires d'événements. Donc si j'ai:

publisher.SomeEvent += target.DoSomething;

a alors publisherune référence àtarget mais pas l'inverse.

Dans votre cas, l'éditeur sera éligible pour le garbage collection (en supposant qu'il n'y ait pas d'autres références à celui-ci), donc le fait qu'il ait une référence aux cibles du gestionnaire d'événements n'est pas pertinent.

Le cas délicat est celui où l'éditeur a une longue durée de vie mais que les abonnés ne veulent pas l'être - dans ce cas, vous devez désabonner les gestionnaires. Par exemple, supposons que vous disposiez d'un service de transfert de données qui vous permette de vous abonner à des notifications asynchrones sur les modifications de bande passante et que l'objet de service de transfert a une longue durée de vie. Si nous faisons ceci:

BandwidthUI ui = new BandwidthUI();
transferService.BandwidthChanged += ui.HandleBandwidthChange;
// Suppose this blocks until the transfer is complete
transferService.Transfer(source, destination);
// We now have to unsusbcribe from the event
transferService.BandwidthChanged -= ui.HandleBandwidthChange;

(Vous voudriez en fait utiliser un bloc finally pour vous assurer de ne pas divulguer le gestionnaire d'événements.) Si nous ne nous désinscrivons pas, alors le service BandwidthUIvivrait au moins aussi longtemps que le service de transfert.

Personnellement, je rencontre rarement cela - généralement si je m'abonne à un événement, la cible de cet événement vit au moins aussi longtemps que l'éditeur - un formulaire durera aussi longtemps que le bouton qui se trouve dessus, par exemple. Cela vaut la peine de connaître ce problème potentiel, mais je pense que certaines personnes s'en inquiètent quand elles n'en ont pas besoin, car elles ne savent pas dans quel sens vont les références.

EDIT: C'est pour répondre au commentaire de Jonathan Dickinson. Tout d'abord, regardez la documentation pour Delegate.Equals (object) qui donne clairement le comportement d'égalité.

Deuxièmement, voici un programme court mais complet pour montrer le fonctionnement de la désinscription:

using System;

public class Publisher
{
    public event EventHandler Foo;

    public void RaiseFoo()
    {
        Console.WriteLine("Raising Foo");
        EventHandler handler = Foo;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
        else
        {
            Console.WriteLine("No handlers");
        }
    }
}

public class Subscriber
{
    public void FooHandler(object sender, EventArgs e)
    {
        Console.WriteLine("Subscriber.FooHandler()");
    }
}

public class Test
{
    static void Main()
    {
         Publisher publisher = new Publisher();
         Subscriber subscriber = new Subscriber();
         publisher.Foo += subscriber.FooHandler;
         publisher.RaiseFoo();
         publisher.Foo -= subscriber.FooHandler;
         publisher.RaiseFoo();
    }
}

Résultats:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

(Testé sur Mono et .NET 3.5SP1.)

Modifier davantage:

Il s'agit de prouver qu'un éditeur d'événement peut être collecté tant qu'il existe encore des références à un abonné.

using System;

public class Publisher
{
    ~Publisher()
    {
        Console.WriteLine("~Publisher");
        Console.WriteLine("Foo==null ? {0}", Foo == null);
    }

    public event EventHandler Foo;
}

public class Subscriber
{
    ~Subscriber()
    {
        Console.WriteLine("~Subscriber");
    }

    public void FooHandler(object sender, EventArgs e) {}
}

public class Test
{
    static void Main()
    {
         Publisher publisher = new Publisher();
         Subscriber subscriber = new Subscriber();
         publisher.Foo += subscriber.FooHandler;

         Console.WriteLine("No more refs to publisher, "
             + "but subscriber is alive");
         GC.Collect();
         GC.WaitForPendingFinalizers();         

         Console.WriteLine("End of Main method. Subscriber is about to "
             + "become eligible for collection");
         GC.KeepAlive(subscriber);
    }
}

Résultats (dans .NET 3.5SP1; Mono semble se comporter un peu bizarrement ici. Nous examinerons cela quelque temps):

No more refs to publisher, but subscriber is alive
~Publisher
Foo==null ? False
End of Main method. Subscriber is about to become eligible for collection
~Subscriber

2
Je suis d'accord avec cela, mais si possible, pouvez-vous expliquer brièvement ou de préférence vous référer à un exemple de ce que vous entendez par «mais les abonnés ne veulent pas être»?
Peter McG

@Jon: Très apprécié, ce n'est pas courant mais comme vous le dites, j'ai vu des gens s'inquiéter inutilement à ce sujet.
Peter McG

- = Ne fonctionne pas. - = entraînera un nouveau délégué, et les délégués ne vérifient pas l'égalité en utilisant la méthode cible, ils font un object.ReferenceEquals () sur le délégué. Le nouveau délégué n'existe pas dans la liste: il n'a aucun effet (et ne renvoie pas assez curieusement une erreur).
Jonathan C Dickinson le

2
@Jonathan: Non, les délégués vérifient l'égalité en utilisant la méthode cible. Prouvera dans une édition.
Jon Skeet

Je concède. Je me suis confondu avec les délégués anonymes.
Jonathan C Dickinson le

8

Dans votre cas, vous allez bien. Au départ, j'ai lu votre question à l'envers, à savoir qu'un abonné sortait du cadre, pas l' éditeur . Si l'éditeur d'événements sort du champ d'application, les références à l'abonné (et non à l'abonné lui-même, bien sûr!) L'accompagnent et il n'est pas nécessaire de les supprimer explicitement.

Ma réponse originale est ci-dessous, à propos de ce qui se passe si vous créez un abonné à un événement et le laissez sortir de la portée sans vous désabonner. Cela ne s'applique pas à votre question, mais je vais la laisser en place pour l'histoire.

Si la classe est toujours enregistrée via des gestionnaires d'événements, elle est toujours accessible. C'est toujours un objet vivant. Un GC suivant un graphe d'événements le trouvera connecté. Oui, vous souhaiterez supprimer explicitement les gestionnaires d'événements.

Ce n'est pas parce que l'objet est hors de portée de son allocation d'origine qu'il est candidat au GC. Tant qu'une référence en direct demeure, elle est en direct.


1
Je ne crois pas qu'une désinscription soit nécessaire ici - le GC voit des références de l'éditeur de l'événement, pas de lui, et c'est l'éditeur qui nous préoccupe ici.
Jon Skeet le

@Jon Skeet: Vous avez raison. J'ai lu la question à l'envers. J'ai corrigé ma réponse pour refléter la réalité.
Eddie le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.