Le garbage collector appellera-t-il IDisposable.Dispose pour moi?

134

Le modèle .NET IDisposable implique que si vous écrivez un finaliseur et 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 juste ceci:

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

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

Oui, je réalise que cela semble stupide, et toute logique implique que ce ne sera pas le cas, mais j'ai toujours eu 2 choses à l'arrière de la tête qui m'ont rendu incertain.

  1. Quelqu'un, il y a quelques années, m'a dit une fois qu'il le ferait en fait, et cette personne avait une expérience très solide de «connaître leurs affaires».

  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 des attributs, etc.), il est donc logique que cela puisse être aussi «magique».

Bien que j'aie lu beaucoup de choses à ce sujet, et qu'il y ait eu beaucoup de choses implicites, je n'ai jamais été en mesure de trouver un réponse définitive par oui ou par non à cette question.

Orion Edwards
la source

Réponses:

121

Le .Net Garbage Collector appelle la méthode Object.Finalize d'un objet sur le garbage collection. Par défaut cela ne fait rien et doit être remplacé si vous souhaitez libérer des ressources supplémentaires.

Dispose n'est PAS automatiquement appelé et doit être explicitement appelé si des 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

Xian
la source
35
En fait, je ne crois pas que le GC appelle Object.Finalize du tout s'il n'est pas remplacé. L'objet est déterminé à ne pas avoir de finaliseur, et la finalisation est supprimée - ce qui le rend plus efficace, car l'objet n'a pas besoin d'être dans les files d'attente de finalisation / atteignables.
Jon Skeet
7
Selon MSDN: msdn.microsoft.com/en-us/library/ ... vous ne pouvez pas réellement "remplacer" la méthode Object.Finalize en C #, le compilateur génère une erreur: ne remplacez pas object.Finalize. Au lieu de cela, fournissez un destructeur. ; c'est-à-dire que vous devez implémenter un destructeur qui agit effectivement en tant que Finalizer. [juste ajouté ici pour être complet car c'est la réponse acceptée et la plus susceptible d'être lue]
Sudhanshu Mishra
1
Le GC ne fait rien à un objet qui ne remplace pas un Finalizer. Il n'est pas placé dans la file d'attente de finalisation - et aucun finaliseur n'est appelé.
Dave Black
1
@dotnetguy - même si la spécification C # originale mentionne un "destructeur", on l'appelle en fait un Finalizer - et sa mécanique est complètement différente de la façon dont un vrai "destructeur" fonctionne pour les langages non gérés.
Dave Black
67

Je tiens à souligner le point de 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 de savoir quand il sera appelé, et même si vous avez suffisamment de mémoire, si jamais il sera appelé.

Mais la mauvaise chose à propos des finaliseurs est que, comme Brian l'a dit, cela fait que votre objet survit à un ramasse-miettes. Cela peut être mauvais. Pourquoi?

Comme vous le savez peut-être ou non, le GC est divisé en générations - Gen 0, 1 et 2, plus le tas d'objets volumineux. Split est un terme vague - vous obtenez un bloc de mémoire, mais il existe des pointeurs indiquant où les objets Gen 0 commencent et se terminent.

Le processus de réflexion est que vous utiliserez probablement beaucoup d'objets qui seront de courte durée. Cela devrait donc être facile et rapide pour le GC d'accéder aux objets de génération 0. Ainsi, lorsqu'il y a une pression de mémoire, la première chose à faire est une collection Gen 0.

Maintenant, si cela ne résout pas suffisamment la pression, il revient en arrière et effectue un balayage Gen 1 (refaire Gen 0), puis si ce n'est toujours pas suffisant, il effectue un balayage Gen 2 (refait Gen 1 et Gen 0). Le nettoyage des objets de longue durée peut donc prendre un certain temps et coûter assez cher (car vos threads peuvent être suspendus pendant l'opération).

Cela signifie que si vous faites quelque chose comme ça:

~MyClass() { }

Votre objet, quoi qu'il arrive, vivra jusqu'à la génération 2. Cela est dû au fait que le GC n'a aucun moyen d'appeler le finaliseur pendant le garbage collection. Ainsi, les objets qui doivent être finalisés sont déplacés vers une file d'attente spéciale pour être nettoyés par un autre thread (le thread de finalisation - qui si vous tuez provoque toutes sortes de mauvaises choses). Cela signifie que vos objets traînent plus longtemps et forcent potentiellement plus de garbage collection.

Donc, tout cela est juste pour ramener à la maison le point que vous souhaitez utiliser IDisposable pour nettoyer les ressources chaque fois que possible et essayer sérieusement de trouver des moyens d'utiliser le finaliseur. C'est dans l'intérêt de votre application.

Cory Foy
la source
8
J'accepte que vous souhaitiez utiliser IDisposable chaque fois que possible, mais vous devriez également avoir un finaliseur qui appelle une méthode dispose. Vous pouvez appeler GC.SuppressFinalize () dans IDispose.Dispose après avoir appelé votre méthode dispose pour vous assurer que votre objet n'est pas placé dans la file d'attente du finaliseur.
jColeson
2
Les générations sont numérotées de 0 à 2 et non de 1 à 3, mais votre publication est par ailleurs bonne. J'ajouterais cependant que tous les objets référencés par votre objet, ou tous les objets référencés par ceux-ci, etc. seront également protégés contre le ramassage des ordures (mais pas contre la finalisation) pour une autre génération. Ainsi, les objets avec des finaliseurs ne doivent pas contenir de références à quoi que ce soit qui n'est pas nécessaire pour la finalisation.
supercat
3
Concernant le "Votre objet, quoi qu'il arrive, vivra jusqu'à la Génération 2." C'est une information TRÈS fondamentale! Cela a permis de gagner beaucoup de temps lors du débogage d'un système, où il y avait beaucoup d'objets Gen2 de courte durée "préparés" pour la finalisation, mais jamais finalisés, provoquant une exception OutOfMemoryException en raison d'une utilisation intensive du tas. En supprimant le finaliseur (même vide) et en déplaçant (contournant) le code ailleurs, le problème a disparu et le GC a pu gérer la charge.
taille-
@CoryFoy "Votre objet, quoi qu'il arrive, vivra jusqu'à la génération 2" Y a-t-il une documentation à ce sujet?
Ashish Negi
33

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

  • Le ramasse-miettes n'exécutera jamais directement une méthode Dispose pour vous.
  • Le GC exécutera les finaliseurs quand il en aura envie.
  • Un modèle courant utilisé pour les objets qui ont un finaliseur consiste à lui faire appeler une méthode définie par convention comme Dispose (bool disposing) passant false pour indiquer que l'appel a été effectué en raison de la finalisation plutôt que d'un appel Dispose explicite.
  • En effet, il n'est pas sûr de faire des hypothèses sur d'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 tromper sur ce modèle.

  • Le contrat pour IDisposable.Dispose indique qu'il doit être sûr d'appeler plusieurs fois (l'appel de Dispose sur un objet qui a déjà été supprimé ne doit rien faire)
  • Il peut devenir 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 lui permettre d'être remplacé afin qu'il puisse être géré, mais je trouve qu'il est sujet aux erreurs.

À mon avis, il vaut mieux éviter complètement d'avoir des types qui contiennent directement à la fois des références jetables et des ressources natives pouvant nécessiter une finalisation. SafeHandles fournit un moyen très propre de le faire en encapsulant des ressources natives dans des ressources 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 en raison d'une exception asynchrone) .

Définir simplement un SafeHandle rend ce Trivial:


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

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

Vous permet de simplifier le type contenant pour:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}
Andrew
la source
1
D'où vient la classe SafeHandleZeroOrMinusOneIsInvalid? Est-ce un type .net intégré?
Orion Edwards
+1 for // À mon avis, il est bien préférable d'éviter complètement d'avoir des types qui contiennent directement à la fois des références jetables et des ressources natives pouvant nécessiter une finalisation .// Les seules classes non scellées qui devraient avoir des finaliseurs sont celles dont le but se concentre sur finalisation.
supercat
1
@OrionEdwards oui voir msdn.microsoft.com/en-us/library/…
Martin Capodici
1
Concernant l'appel à GC.SuppressFinalizedans cet exemple. Dans ce contexte, SuppressFinalize ne doit être appelé que s'il Dispose(true)s'exécute avec succès. Si Dispose(true)échoue à un moment donné après que la finalisation est supprimée mais avant que toutes les ressources (en particulier celles non gérées) ne soient nettoyées, vous voulez toujours que la finalisation se produise afin de faire autant de nettoyage que possible. Mieux vaut déplacer l' GC.SuppressFinalizeappel dans la Dispose()méthode après l'appel à Dispose(true). Voir les directives de conception du cadre et cet article .
BitMask777
6

Je ne pense pas. Vous avez le contrôle sur le moment où Dispose est appelé, ce qui signifie que vous pouvez en théorie écrire du code d'élimination qui émet des hypothèses 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 douteux que le finaliseur appelle automatiquement Dispose en votre nom.


EDIT: Je suis parti et j'ai testé, juste pour m'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");
    }
}
Matt Bishop
la source
Faire des hypothèses sur les objets disponibles lors de la mise au rebut peut être dangereux et délicat, en particulier lors de la finalisation.
Scott Dorman
3

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

TOUTEFOIS. Au prochain garbage collection, au lieu d'être collecté, l'objet ira dans la fin de finalisation, tout sera collecté, puis son finaliseur sera appelé. La prochaine collection après cela sera libérée.

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

Brian Leahy
la source
1

Non, ça ne s'appelle pas.

Mais cela rend facile de ne pas oublier de disposer vos objets. Utilisez simplement le usingmot - clé.

J'ai fait le test suivant pour cela:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo();
        foo = null;
        Console.WriteLine("foo is null");
        GC.Collect();
        Console.WriteLine("GC Called");
        Console.ReadLine();
    }
}

class Foo : IDisposable
{
    public void Dispose()
    {

        Console.WriteLine("Disposed!");
    }
Penyaskito
la source
1
C'était un exemple de la façon dont si vous n'utilisez PAS le mot-clé <code> using </code>, il ne sera pas appelé ... et cet extrait a 9 ans, joyeux anniversaire!
penyaskito
1

Le GC n'appellera pas disposer. Il peut appeler votre finaliseur, mais même cela n'est pas garanti dans toutes les circonstances.

Consultez cet article pour une discussion sur la meilleure façon de gérer cela.

Rob Walker
la source
0

La documentation sur IDisposable donne une explication assez claire et détaillée du comportement, ainsi que des exemples de code. Le GC n'appellera PAS la Dispose()méthode sur l'interface, mais il appellera le finaliseur pour votre objet.

Joseph Daigle
la source
0

Le modèle IDisposable a été créé principalement pour être appelé par le développeur, si vous avez un objet qui implémente IDispose, le développeur doit soit implémenter le using mot clé autour du contexte de l'objet, soit appeler directement la méthode Dispose.

La solution de sécurité pour le modèle consiste à implémenter le finaliseur appelant la méthode Dispose (). Si vous ne le faites pas, vous risquez de créer des fuites de mémoire, c'est-à-dire: Si vous créez un wrapper COM et que vous n'appelez jamais System.Runtime.Interop.Marshall.ReleaseComObject (comObject) (qui serait placé dans la méthode Dispose).

Il n'y a pas de magie dans le clr pour appeler automatiquement des méthodes Dispose autre que le suivi des objets qui contiennent des finaliseurs et leur stockage dans la table Finalizer par le GC et leur appel lorsque certaines heuristiques de nettoyage entrent en jeu par le GC.

Erick Sgarbi
la source