web-dev-qa-db-fra.com

Quelles sont les meilleures pratiques pour nettoyer les références de gestionnaires d'événements?

Souvent, je me retrouve à écrire un code comme celui-ci:

        if (Session != null)
        {
            Session.KillAllProcesses();
            Session.AllUnitsReady -= Session_AllUnitsReady;
            Session.AllUnitsResultsPublished -= Session_AllUnitsResultsPublished;
            Session.UnitFailed -= Session_UnitFailed;
            Session.SomeUnitsFailed -= Session_SomeUnitsFailed;
            Session.UnitCheckedIn -= Session_UnitCheckedIn;
            UnattachListeners();
        }

Le but est de nettoyer tous les abonnements aux événements pour lesquels nous avons été enregistrés sur la cible (Session) afin que cette session puisse être supprimée par le GC. Cependant, j'ai discuté avec un collègue au sujet des classes qui implémentent IDisposable. Il était convaincu que ces classes devaient effectuer un nettoyage préalable de la manière suivante:

    /// <summary>
    /// Disposes the object
    /// </summary>
    public void Dispose()
    {
        SubmitRequested = null; //frees all references to the SubmitRequested Event
    }

Y a-t-il une raison pour préférer l'un par rapport à l'autre? Y a-t-il une meilleure façon de s'y prendre tout à fait? (Mis à part les événements de référence faibles partout)

Ce que j'aimerais vraiment voir, c’est quelque chose qui ressemble au modèle d’appel sécurisé pour déclencher des événements: c’est-à-dire sûr et reproductible. Quelque chose que je peux me rappeler de faire chaque fois que je m'attache à un événement pour pouvoir m'assurer qu'il sera facile à nettoyer pour moi.

30
Firoso

Il est incorrect de dire que la désinscription des gestionnaires des événements Session permettra en quelque sorte à un objet Session d'être collecté par le GC. Voici un diagramme qui illustre la chaîne de référence des événements.

--------------      ------------      ----------------
|            |      |          |      |              |
|Event Source|  ==> | Delegate |  ==> | Event Target |
|            |      |          |      |              |
--------------      ------------      ----------------

Donc, dans votre cas, la source de l'événement est un objet Session. Mais je ne vois pas que vous ayez mentionné quelle classe a déclaré les gestionnaires, nous ne savons donc pas encore qui est la cible de l'événement. Permet d’envisager deux possibilités. La cible de l'événement peut être le même objet Session qui représente la source ou une classe entièrement distincte. Dans les deux cas et dans des circonstances normales, la Session sera collectée tant qu'il n'y aura pas de référence supplémentaire, même si les gestionnaires de ses événements restent enregistrés. En effet, le délégué ne contient pas de référence à la source de l'événement. Il ne contient qu'une référence à la cible de l'événement.

Considérons le code suivant.

public static void Main()
{
  var test1 = new Source();
  test1.Event += (sender, args) => { Console.WriteLine("Hello World"); };
  test1 = null;
  GC.Collect();
  GC.WaitForPendingFinalizers();

  var test2 = new Source();
  test2.Event += test2.Handler;
  test2 = null;
  GC.Collect();
  GC.WaitForPendingFinalizers();
}

public class Source()
{
  public event EventHandler Event;

  ~Source() { Console.WriteLine("disposed"); }

  public void Handler(object sender, EventArgs args) { }
}

Vous verrez que "disposé" est imprimé deux fois sur la console en vérifiant que les deux instances ont été collectées sans annuler l'enregistrement de l'événement. La collecte de l'objet référencé par test2 est due au fait qu'il reste une entité isolée dans le graphe de référence (une fois que test2 est défini sur null) même s'il contient une référence à lui-même malgré l'événement.

À présent, la situation est délicate lorsque vous souhaitez que la cible d’événement ait une durée de vie plus courte que la source de l’événement. Dans ce cas, vous avez pour désenregistrer les événements. Considérez le code suivant qui illustre cela.

public static void Main()
{
  var parent = new Parent();
  parent.CreateChild();
  parent.DestroyChild();
  GC.Collect();
  GC.WaitForPendingFinalizers();
}

public class Child
{
  public Child(Parent parent)
  {
    parent.Event += this.Handler;
  }

  private void Handler(object sender, EventArgs args) { }

  ~Child() { Console.WriteLine("disposed"); }
}

public class Parent
{
  public event EventHandler Event;

  private Child m_Child;

  public void CreateChild()
  {
    m_Child = new Child(this);
  }

  public void DestroyChild()
  {
    m_Child = null;
  }
}

Vous verrez que "éliminé" n'est jamais imprimé sur la console, ce qui démontre une éventuelle fuite de mémoire. C'est un problème particulièrement difficile à traiter. L'implémentation de IDisposable dans Child ne résoudra pas le problème, car rien ne garantit que les appelants joueront correctement et appelleront réellement Dispose.

La réponse

Si votre source d'événement implémente IDisposable, vous ne vous êtes pas acheté de nouveaux objets. En effet, si la source d'événements n'est plus enracinée, la cible de l'événement ne le sera plus.

Si votre cible d'événement implémente IDisposable, elle pourrait se dégager de la source de l'événement, mais rien ne garantit que Dispose sera appelé.

Je ne dis pas que les événements non enregistrés de Dispose sont faux. Mon point est que vous devez vraiment examiner la définition de votre hiérarchie de classes et envisager le meilleur moyen d’éviter le problème des fuites de mémoire, le cas échéant.

40
Brian Gideon

L'implémentation d'IDisposable présente deux avantages par rapport à la méthode manuelle:

  1. C'est standard et le compilateur le traite spécialement. Cela signifie que tout le monde qui lit votre code comprend ce qui se passe dès qu’il voit la mise en œuvre d’Idisposable.
  2. .NET C # et VB fournissent des structures spéciales pour travailler avec IDisposable via l'instruction using

Néanmoins, je doute que cela soit utile dans votre scénario. Pour éliminer en toute sécurité un objet, il doit être jeté dans le bloc-final à l'intérieur d'un try/catch. Dans le cas que vous semblez décrire, il peut être nécessaire que soit la session, soit le code l’appelant, lors de la suppression de l’objet (c’est-à-dire à la fin de sa portée: dans le dernier bloc). Si tel est le cas, la session doit également implémenter IDisposable, qui suit le concept commun. Dans la méthode IDisposable.Dispose, il parcourt tous ses membres jetables et les élimine.

Modifier

Votre dernier commentaire me fait repenser ma réponse et essayer de relier quelques points. Vous voulez vous assurer que Session est disponible pour le GC. Si les références aux délégués proviennent de la même classe, il n'est pas du tout nécessaire de les désabonner. S'ils appartiennent à une autre classe, vous devez les désabonner. En regardant le code ci-dessus, vous semblez écrire ce bloc de code dans n'importe quelle classe qui utilise Session et le nettoyer à un moment donné du processus.

Si Session doit être libérée, il existe un moyen plus direct où l'appel de la classe n'a pas à être responsable du traitement correct du processus de désabonnement. Bouclez simplement tous les événements en utilisant une réflexion triviale et définissez-les tous comme nuls (vous pouvez envisager d'autres approches pour atteindre le même effet).

Comme vous demandez des "meilleures pratiques", vous devez associer cette méthode à IDisposable et implémenter la boucle dans IDisposable.Dispose(). Avant de vous lancer dans cette boucle, vous appelez un événement supplémentaire: Disposing, que les auditeurs peuvent utiliser s'ils ont besoin de nettoyer eux-mêmes. Lorsque vous utilisez IDisposable, soyez conscient de ses mises en garde, dont ce modèle décrit brièvement est une solution courante.

3
Abel

Le modèle de gestion des événements généré automatiquement à l'aide du mot clé vb.net WithEvents est plutôt correct. Le code VB (à peu près):

 WithEvents myPort As SerialPort 
 
 Sub GotData (Sender As Object, e as DataReceivedEventArgs) Gère myPort.DataReceived 
 Sub SawPinChange (Sender As Object, e comme DataReceivedEventArgs) gère myPort.DataReceived 
 Sub SawPinChange (Sender As Object, e comme PinChangedEventArgs). Gère myPort.PinChanged 

sera traduit en équivalent de:

 SerialPort _myPort; 
 SerialPort myPort 
 {Get {return _myPort; } 
 set {
 if (_myPort! = null) 
 {
 _myPort.DataReceived - = GotData; 
 _myPort.PinChanged - = SawPinChange; 
} 
 _myPort = valeur; 
 if (_myPort! = null) 
 {
 _myPort.DataReceived + = GotData; 
 _myPort.PinChanged + = SawPinChange; 
} 
} 
} 

C'est un modèle raisonnable à suivre; Si vous utilisez ce modèle, dans Dispose, vous définissez sur null toutes les propriétés associées à des événements, qui se chargeront ensuite de les désabonner.

Si vous souhaitez automatiser légèrement l'élimination, afin de vous assurer que les choses sont bien disposées, vous pouvez modifier la propriété pour qu'elle ressemble à ceci:

 Action <myType> myCleanups; // Une seule fois pour toute la classe 
 SerialPort _myPort; 
 Static void cancel_myPort (monType x) {x.myPort = null;} 
 SerialPort monPort 
 {Get {return _myPort; } 
 set {
 if (_myPort! = null) 
 {
 _myPort.DataReceived - = GotData; 
 _myPort.PinChanged - = SawPinChange; 
 myCleanups - = cancel_myPort; 
} 
 _myPort = value; 
 if (_myPort! = null) 
 {
 myCleanups + = cancel_myPort; 
 _myPort.DataReceived + = GotData; 
 _myPort.PinChanged + = SawPinChange; 
} 
} 
} 
 // Plus tard, dans Dispose ... 
 MyCleanups (this); // Effectuer des nettoyages en file d'attente 

Notez que le fait d'associer des délégués statiques à myCleanups signifie que même s'il existe plusieurs instances de myClass, il ne devra y avoir qu'un seul exemplaire de chaque délégué à l'échelle du système. Peut-être pas un gros problème pour les classes avec peu d’instances, mais potentiellement significatif si une classe sera instanciée plusieurs milliers de fois.

3
supercat

Ma préférence est de gérer la vie avec un jetable. Rx comprend des extensions jetables qui vous permettent d'effectuer les tâches suivantes:

Disposable.Create(() => {
                this.ViewModel.Selection.CollectionChanged -= SelectionChanged;
            })

Si vous stockez ensuite cela dans une sorte de GroupDisposable qui est défini à la bonne portée, tous les éléments sont définis.

Si vous ne gérez pas la durée de vie à l'aide de produits jetables et de portées, vous devez absolument vous renseigner, car cela devient un modèle très répandu dans .net.

2
DanH

La globalisation des événements les plus couramment utilisés dans sa propre classe et l'héritage de leurs interfaces permettent aux développeurs d'utiliser des méthodes telles que les propriétés d'événement pour ajouter et supprimer des événements. Dans votre classe, que l'encapsulation se produise ou non, vous pouvez commencer par utiliser quelque chose de similaire à l'exemple ci-dessous.

Par exemple.

#region Control Event Clean up
private event NotifyCollectionChangedEventHandler CollectionChangedFiles
{
    add { FC.CollectionChanged += value; }
    remove { FC.CollectionChanged -= value; }
}
#endregion Control Event Clean up

Cet article fournit des informations supplémentaires sur d'autres utilisations de la propriété ADD REMOVE: http://msdn.Microsoft.com/en-us/library/8843a9ch.aspx

1
Apprehensivegent

La réponse de DanH est presque là, mais il lui manque un élément crucial.

Pour que cela fonctionne toujours correctement, vous devez d'abord prendre une copie locale de la variable, au cas où elle changerait. Essentiellement, nous devons appliquer une fermeture implicitement capturée.

List<IDisposable> eventsToDispose = new List<IDisposable>();

var handlerCopy = this.ViewModel.Selection;
eventsToDispose.Add(Disposable.Create(() => 
{
    handlerCopy.CollectionChanged -= SelectionChanged;
}));

Et plus tard, nous pouvons disposer de tous les événements en utilisant ceci:

foreach(var d in eventsToDispose)
{ 
    d.Dispose();
}

Si nous voulons le rendre plus court:

eventsToDispose.ForEach(o => o.Dispose());

Si nous voulons le rendre encore plus court, nous pouvons remplacer IList par CompositeDisposable, qui est exactement la même chose dans les coulisses.

Ensuite, nous pouvons disposer de tous les événements avec ceci:

eventsToDispose.Dispose();
0
Contango