126 votes

Le collecteur de déchets appellera-t-il IDisposable.Dispose pour moi ?

Le programme .NET IDisposable Pattern implique que si vous écrivez un finaliseur et que vous implémentez IDisposable, votre finaliseur doit appeler explicitement Dispose. C'est logique et c'est ce que j'ai toujours fait dans les rares situations où un finaliseur est justifié.

Cependant, que se passe-t-il si je fais simplement ceci :

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

et n'implémente pas de finaliseur, ou quoi que ce soit d'autre. Le framework appellera-t-il la méthode Dispose pour moi ?

Oui, je suis conscient que cela semble stupide et que toute logique implique que ce ne sera pas le cas, mais j'ai toujours eu deux choses à l'esprit qui m'ont rendu incertain.

  1. Il y a quelques années, quelqu'un m'a dit que c'était effectivement le cas, et cette personne avait de très bons antécédents en matière de connaissances.

  2. Le compilateur/framework fait d'autres choses "magiques" en fonction des interfaces que vous implémentez (par exemple : foreach, méthodes d'extension, sérialisation basée sur les attributs, etc), il est donc logique que cela soit "magique" aussi.

Bien que j'aie lu beaucoup de choses à ce sujet, et que beaucoup de choses aient été sous-entendues, je n'ai jamais été en mesure de trouver une définitif Réponse par oui ou par non à cette question.

112voto

Xian Points 33986

Le collecteur de déchets .Net appelle la méthode Object.Finalize d'un objet lors de la collecte des déchets. Par par défaut ceci fait rien et doit être surchargé si vous souhaitez libérer des ressources supplémentaires.

Dispose n'est PAS automatiquement appelé et doit être explicite appelé si les ressources doivent être libérées, par exemple dans un bloc "using" ou "try finally".

voir http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx pour plus d'informations

62voto

Cory Foy Points 5181

Je voudrais insister sur le point soulevé par Brian dans son commentaire, car il est important.

Les finaliseurs ne sont pas des destructeurs déterministes comme en C++. Comme d'autres l'ont souligné, il n'y a aucune garantie quant au moment où il sera appelé, et même si vous avez suffisamment de mémoire, s'il sera appelé, il n'y a aucune garantie quant au moment où il sera appelé, ni quant au moment où il sera appelé. toujours s'appelle.

Mais la mauvaise chose à propos des finaliseurs est que, comme l'a dit Brian, cela fait survivre votre objet à une collecte de déchets. Cela peut être mauvais. Pourquoi ?

Comme vous le savez ou non, le GC est divisé en générations - Gen 0, 1 et 2, plus le Large Object Heap. La division est un terme vague - vous obtenez un bloc de mémoire, mais il existe des pointeurs indiquant où commencent et où finissent les objets de la génération 0.

L'idée est que vous utiliserez probablement beaucoup d'objets qui seront de courte durée. Ces objets devraient donc être faciles et rapides à atteindre pour le GC - les objets de la génération 0. Ainsi, lorsqu'il y a une pression sur la mémoire, la première chose qu'il fait est une collection Gen 0.

Si cela ne suffit pas à résoudre le problème de la pression, le système revient en arrière et effectue un balayage de la Génération 1 (en refaisant la Génération 0), et si cela ne suffit toujours pas, il effectue un balayage de la Génération 2 (en refaisant la Génération 1 et la Génération 0). Ainsi, le nettoyage des objets de longue durée peut prendre un certain temps et s'avérer assez coûteux (puisque vos threads peuvent être suspendus pendant l'opération).

Cela signifie que si vous faites quelque chose comme ceci :

~MyClass() { }

Votre objet, quoi qu'il arrive, vivra jusqu'à la génération 2. En effet, le GC n'a aucun moyen d'appeler le finalisateur pendant le ramassage des ordures. Les objets qui doivent être finalisés sont donc placés dans une file d'attente spéciale pour être nettoyés par un autre thread (le thread du finaliseur - qui, si vous le tuez, provoque toutes sortes de problèmes). Cela signifie que vos objets restent plus longtemps dans le système, ce qui peut entraîner un plus grand nombre de collectes d'ordures.

Tout cela n'a d'autre but que d'insister sur le fait qu'il faut utiliser IDisposable pour nettoyer les ressources chaque fois que c'est possible et essayer sérieusement de trouver des moyens de contourner l'utilisation du finalisateur. C'est dans l'intérêt de votre application.

31voto

Andrew Points 1294

Il y a déjà beaucoup de bonnes discussions ici, et je suis un peu en retard, mais je voulais ajouter quelques points moi-même.

  • Le collecteur de déchets n'exécutera jamais directement une méthode Dispose à votre place.
  • Le CG volonté exécute les finaliseurs lorsqu'il en a envie.
  • Un modèle courant utilisé pour les objets dotés d'un finaliseur consiste à appeler une méthode définie par convention comme Dispose(bool disposing) en passant false pour indiquer que l'appel a été effectué en raison d'une finalisation plutôt que d'un appel explicite à Dispose.
  • En effet, il n'est pas sûr de faire des suppositions sur les autres objets gérés lors de la finalisation d'un objet (ils peuvent avoir déjà été finalisés).

    class SomeObject : IDisposable { IntPtr _SomeNativeHandle; FileStream _SomeFileStream;

    // Something useful here

    ~ SomeObject() { Dispose(false); }

    public void Dispose() { Dispose(true); }

    protected virtual void Dispose(bool disposing) { if(disposing) { GC.SuppressFinalize(this); //Because the object was explicitly disposed, there will be no need to //run the finalizer. Suppressing it reduces pressure on the GC

    //The managed reference to an IDisposable is disposed only if the _SomeFileStream.Dispose(); }

    //Regardless, clean up the native handle ourselves. Because it is simple a member // of the current instance, the GC can't have done anything to it, // and this is the onlyplace to safely clean up

    if(IntPtr.Zero != _SomeNativeHandle) { NativeMethods.CloseHandle(_SomeNativeHandle); _SomeNativeHandle = IntPtr.Zero; } } }

C'est la version simple, mais il y a beaucoup de nuances qui peuvent vous faire trébucher sur ce modèle.

  • Le contrat de IDisposable.Dispose indique qu'il doit être possible de l'appeler plusieurs fois en toute sécurité (appeler Dispose sur un objet qui a déjà été éliminé ne devrait rien faire).
  • Il peut s'avérer très compliqué de gérer correctement une hiérarchie d'héritage d'objets jetables, en particulier si différentes couches introduisent de nouvelles ressources jetables et non gérées. Dans le modèle ci-dessus, Dispose(bool) est virtuel pour permettre de le surcharger afin qu'il puisse être géré, mais je trouve que c'est une source d'erreurs.

À mon avis, il vaut mieux éviter complètement d'avoir des types qui contiennent directement des références jetables et des ressources natives qui peuvent nécessiter une finalisation. Les SafeHandles fournissent un moyen très propre de le faire en encapsulant les ressources natives dans des jetables qui fournissent en interne leur propre finalisation (avec un certain nombre d'autres avantages comme la suppression de la fenêtre pendant P/Invoke où un handle natif pourrait être perdu à cause d'une exception asynchrone).

La simple définition d'un SafeHandle rend cette opération triviale :

private class SomeSafeHandle
 : SafeHandleZeroOrMinusOneIsInvalid {
 public SomeSafeHandle()
  : base(true)
  { }

 protected override bool ReleaseHandle()
 { return NativeMethods.CloseHandle(handle); }
}

Permet de simplifier le type de contenant :

class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}

6voto

Matt Bishop Points 1187

Je ne pense pas que ce soit le cas. Vous avez le contrôle sur le moment où Dispose est appelé, ce qui signifie que vous pouvez en théorie écrire un code d'élimination qui fait des suppositions sur (par exemple) l'existence d'autres objets. Vous n'avez aucun contrôle sur le moment où le finaliseur est appelé, il serait donc difficile de faire en sorte que le finaliseur appelle automatiquement Dispose en votre nom.


EDIT : Je suis allé tester, juste pour m'en assurer :

class Program
{
    static void Main(string[] args)
    {
        Fred f = new Fred();
        f = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Fred's gone, and he's not coming back...");
        Console.ReadLine();
    }
}

class Fred : IDisposable
{
    ~Fred()
    {
        Console.WriteLine("Being finalized");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("Being Disposed");
    }
}

3voto

Brian Leahy Points 7840

Pas dans le cas que vous décrivez, Mais le GC appellera la fonction Finalizer pour vous, si vous en avez un.

TOUTEFOIS. Lors de la prochaine collecte de déchets, au lieu d'être collecté, l'objet passera dans la queue de finalisation, tout sera collecté, puis son finalisateur sera appelé. Lors de la prochaine collecte, l'objet sera libéré.

En fonction de la pression de mémoire de votre application, il se peut que vous n'ayez pas de gc pour cette génération d'objets pendant un certain temps. Ainsi, dans le cas d'un flux de fichiers ou d'une connexion à une base de données, il se peut que vous deviez attendre un certain temps pour que la ressource non gérée soit libérée lors de l'appel au finaliseur, ce qui peut poser des problèmes.

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