web-dev-qa-db-fra.com

Avez-vous besoin de supprimer un gestionnaire d'événements dans le destructeur?

J'utilise des UserControls qui sont créés et détruits dans mon application pendant l'exécution (en créant et en fermant des sous-fenêtres avec ces contrôles à l'intérieur).
Il s'agit d'un WPF UserControl et hérite de System.Windows.Controls.UserControl. Il n'y a pas de méthode Dispose() que je puisse remplacer.
PPMM est un Singleton avec la même durée de vie que mon application.
Maintenant dans le constructeur de mon (WPF) UserControl, j'ajoute un gestionnaire d'événements:

public MyControl()
{
    InitializeComponent();

    // hook up to an event
    PPMM.FactorChanged += new ppmmEventHandler(PPMM_FactorChanged);
}

Je me suis habitué à supprimer un tel gestionnaire d'événements dans le destructeur:

~MyControl()
{
    // hook off of the event
    PPMM.FactorChanged -= new ppmmEventHandler(PPMM_FactorChanged);
}

Aujourd'hui, je suis tombé sur cela et je me suis demandé:

1) Est-ce nécessaire? Ou le GC s'en occupe-t-il?

2) Est-ce que cela fonctionne même? Ou devrais-je stocker le ppmmEventHandler nouvellement créé?

J'attends vos réponses avec impatience.

51
Martin Hennings

Puisque PPMM est un objet à longue durée de vie (singleton), alors ce code n'a pas beaucoup de sens.

Le problème ici est que tant que ce gestionnaire d'événements référencera l'objet, , il ne sera pas éligible pour le garbage collection , au moins aussi longtemps que cet autre l'objet propriétaire de l'événement est vivant.

En tant que tel, mettre quoi que ce soit dans le destructeur est inutile, car:

  1. Le gestionnaire d'événements a déjà été supprimé, donc l'objet est devenu éligible pour la récupération de place
  2. Le gestionnaire d'événements n'est pas supprimé, l'objet propriétaire n'est pas éligible pour le garbage collection, et donc le finaliseur ne sera jamais appelé
  3. Les deux objets sont éligibles pour la récupération de place, auquel cas vous ne devez pas accéder à cet autre objet dans le finaliseur car vous ne connaissez pas son état interne

En bref, ne faites pas ça.

Maintenant, un argument différent pourrait être dit à propos de l'ajout d'un tel code à la méthode Dispose, lorsque vous implémentez IDisposable. Dans ce cas , cela a tout son sens puisque son code utilisateur qui appelle Dispose, à un point prédéfini et contrôlé.

Le finaliseur (destructeur), cependant, n'est appelé que lorsque l'objet est éligible pour le ramasse-miettes et possède un finaliseur, auquel cas cela ne sert à rien.

Quant à la question nbr. 2, que je considère comme "Puis-je me désinscrire d'événements comme ça", alors oui, vous pouvez. La seule fois où vous devez conserver le délégué avec lequel vous vous abonniez, c'est lorsque vous construisez le délégué autour d'une méthode anonyme ou d'une expression lambda. Lorsque vous le construisez autour d'une méthode existante, cela fonctionne.


Modifier: WPF. à droite, n'a pas vu cette balise. Désolé, le reste de ma réponse n'a pas beaucoup de sens pour WPF et comme je ne suis pas un gourou WPF, je ne peux pas vraiment le dire. Cependant, il existe un moyen de résoudre ce problème. C'est tout à fait légal ici sur SO pour braconner le contenu d'une autre réponse si vous pouvez l'améliorer. Donc, si quelqu'un sait comment le faire correctement avec un contrôle utilisateur WPF, vous êtes libre de soulever l'ensemble première section de ma réponse et ajoutez les bits pertinents de WPF.

Modifier: Permettez-moi de répondre également à la question dans le commentaire ici.

Étant donné que la classe en question est un contrôle utilisateur, sa durée de vie sera liée à un formulaire. Lorsque le formulaire se ferme, il disposera de tous les contrôles enfants qu'il possède, en d'autres termes il existe déjà une méthode Dispose ici .

La façon correcte pour un contrôle utilisateur de gérer cela, s'il gère ses propres événements, consiste à décrocher les gestionnaires d'événements dans la méthode Dispose.

(reste retiré)

34

WPF ne prend pas bien en charge IDisposable. Si vous implémentez un contrôle WPF qui nécessite un nettoyage, vous devriez penser à vous connecter aux événements Loaded et Unloaded à la place (ou en outre).

C'est à dire. vous vous connectez à l'événement dans le gestionnaire Loaded et vous vous déconnectez dans le gestionnaire Unloaded. Bien sûr, ce n'est qu'une option si votre contrôle n'a pas besoin de recevoir l'événement alors qu'il n'est pas "chargé" et si vous pouvez correctement prendre en charge de nombreux cycles de chargement/déchargement.

L'avantage d'utiliser les événements Loaded/Unloaded est que vous n'avez pas à supprimer manuellement le contrôle utilisateur partout où il est utilisé. Vous devez cependant savoir que l'événement Unloaded n'est pas déclenché après le démarrage de l'arrêt de l'application. Par exemple. si votre mode d'arrêt est OnMainWindowClose, les événements Unloaded pour les autres fenêtres ne seront pas déclenchés. Ce n'est généralement pas un problème cependant. Cela signifie simplement que vous ne pouvez pas faire des choses de manière fiable dans un Unloaded qui doivent se produire avant/pendant la fermeture de l'application.

8
Paul Groke

Tout d'abord, je dirais de ne pas utiliser un destructeur mais Dispose () pour effacer vos ressources.

Deuxièmement, à mon avis, si ce code est à l'intérieur d'un objet qui est créé très souvent et a une courte durée de vie, il est préférable de prendre soin de supprimer le gestionnaire d'événements vous-même car il s'agit d'un lien vers l'objet titulaire, ce qui empêchera le GC de la collecte .

Cordialement.

7
Tigran

Si le code parvient au destructeur, cela n'a plus d'importance.

C'est parce qu'il ne sera détruit que s'il n'écoute plus aucun événement.
S'il écoutait toujours les événements, il n'aurait pas été détruit.

2
Yochai Timmer

PPMM est-il externe avec une durée de vie plus longue que les instances de MyControl?

Si oui, à moins que PPMM_FactorChanged est une méthode statique, ppmmEventHandler conservera une référence à l'instance MyControl en direct - ce qui signifie que l'instance ne sera jamais éligible pour le garbage collection et que le finaliseur ne se déclenchera jamais.

Vous ne devriez pas avoir besoin de conserver le ppmmEventHandler pour le code de suppression.

2

Le GC s'en chargera. Bien que l'événement contienne une référence forte, il ne la détient que sur l'objet parent lui-même. Au final, seul MyControl conservera une référence via le gestionnaire d'événements, donc le GC la collectera.

D'un autre côté, utiliser le finaliseur, qui n'est PAS un descruteur. C'est pour cette mauvaise pratique. Si vous souhaitez annuler l'enregistrement d'un événement, vous devez envisager IDisposable.

2
dowhilefor

Les gestionnaires d'événements sont délicats et peuvent facilement masquer une fuite de ressources. Comme le dit Tigran. Utilisez IDisposeable et oubliez les destructeurs. Je recommande de mesurer si vous avez bien compris. Il suffit de regarder la consommation de mémoire de votre application dans le gestionnaire de tâches pour savoir si une fuite est présente si vous testez un peu la tension en chargeant et en fermant quelques milliers de fenêtres.

1
Esben Skov Pedersen

Il y a des cas où la désinscription d'un événement dans un Finalizer/destructor pourrait être utile, si l'éditeur d'événement a garanti que la désinscription était thread-safe. Pour qu'un objet se désabonne de ses événements propres, cela serait inutile, mais on pourrait, en tant que modèle réalisable, qu'un objet orienté vers le public contienne une référence à un objet privé qui "fait tout le travail", et demander à cet objet privé de s'abonner aux événements. S'il n'y a aucune référence de l'objet privé à l'objet public, l'objet public deviendra éligible à la finalisation une fois que personne ne s'y intéressera encore; son finaliseur serait alors en mesure d'annuler l'abonnement au nom de l'objet privé.

Malheureusement, ce modèle ne peut fonctionner que si les objets dont les événements sont abonnés garantissent qu'il peut accepter les demandes de désabonnement de n'importe quel contexte de thread, et pas seulement le contexte dans lequel les événements ont été abonnés. Le fait que .NET exige dans le cadre du contrat "événement" que toutes les méthodes de désabonnement doivent être thread-safe n'aurait pas imposé une charge majeure aux implémentations, mais pour quelque raison que ce soit, MS n'a pas imposé une telle exigence. Par conséquent, même si un Finalizer/destructeur découvre qu'un événement doit être désabonné le plus tôt possible, il n'y a pas de mécanisme standard par lequel il peut y arriver.

1
supercat

2) Cela fonctionne

1) J'ai eu un cas (avec un service de messagerie intégré) que les gestionnaires d'événements vers un objet global n'étaient pas libérés et le GC n'a pas pu collecter l'objet à cause de cela. Je pense que c'est généralement une condition rare - en utilisant un profileur comme ANTS from red gate, vous pouvez facilement faire un profilage de la mémoire si vous pensez que cela vous arrive.

0
Sascha