web-dev-qa-db-fra.com

Définition d'un objet sur null vs Dispose ()

Je suis fasciné par le fonctionnement du CLR et du GC (je travaille à élargir mes connaissances à ce sujet en lisant CLR via C #, les livres/articles de Jon Skeet, et plus encore).

Quoi qu'il en soit, quelle est la différence entre dire:

MyClass myclass = new MyClass();
myclass = null;

Ou, en faisant en sorte que MyClass implémente IDisposable et un destructeur et en appelant Dispose ()?

De plus, si j'ai un bloc de code avec une instruction using (par exemple ci-dessous), si je parcours le code et quitte le bloc using, l'objet est-il supprimé alors ou quand un garbage collection se produit? Que se passerait-il si j'appelais Dispose () dans le bloc using de toute façon?

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

Les classes de flux (par exemple BinaryWriter) ont une méthode Finalize? Pourquoi voudrais-je utiliser ça?

106
GurdeepS

Il est important de séparer l'élimination de la collecte des ordures. Ce sont des choses complètement distinctes, avec un point commun que j'aborderai dans une minute.

Dispose, garbage collection et finalisation

Lorsque vous écrivez une instruction using, il s'agit simplement de sucre syntaxique pour un bloc try/finally afin que Dispose soit appelé même si le code dans le corps de l'instruction using renvoie un exception. Cela ne le fait pas signifie que l'objet est récupéré à la fin du bloc.

L'élimination concerne environ ressources non gérées (ressources autres que la mémoire). Il peut s'agir de descripteurs d'interface utilisateur, de connexions réseau, de descripteurs de fichiers, etc. Vous devez implémenter IDisposable chaque fois que votre type "possède" une ressource non gérée, soit directement (généralement via un IntPtr) soit indirectement (par exemple via un Stream, un SqlConnection etc).

La collecte des ordures elle-même ne concerne que la mémoire - avec une petite torsion. Le garbage collector est capable de trouver des objets qui ne peuvent plus être référencés et de les libérer. Cependant, il ne recherche pas les ordures tout le temps - uniquement lorsqu'il détecte qu'il en a besoin (par exemple, si une "génération" du tas manque de mémoire).

La torsion est finalisation. Le garbage collector conserve une liste d'objets qui ne sont plus accessibles, mais qui ont un finaliseur (écrit comme ~Foo() en C #, quelque peu confus - ils ne ressemblent en rien aux destructeurs C++). Il exécute les finaliseurs sur ces objets, juste au cas où ils auraient besoin de faire un nettoyage supplémentaire avant que leur mémoire ne soit libérée.

Les finaliseurs sont presque toujours utilisés pour nettoyer les ressources dans le cas où l'utilisateur du type a oublié de les éliminer de manière ordonnée. Donc, si vous ouvrez un FileStream mais oubliez d'appeler Dispose ou Close, le finaliseur éventuellement libérera le descripteur de fichier sous-jacent pour vous. Dans un programme bien écrit, les finaliseurs ne devraient presque jamais se déclencher à mon avis.

Définition d'une variable sur null

Un petit point sur la définition d'une variable sur null - ce n'est presque jamais nécessaire pour le garbage collection. Vous pouvez parfois vouloir le faire s'il s'agit d'une variable membre, bien que selon mon expérience, il est rare qu'une "partie" d'un objet ne soit plus nécessaire. Lorsqu'il s'agit d'une variable locale, le JIT est généralement assez intelligent (en mode release) pour savoir quand vous n'utiliserez plus une référence. Par exemple:

StringBuilder sb = new StringBuilder();
sb.Append("Foo");
string x = sb.ToString();

// The string and StringBuilder are already eligible
// for garbage collection here!
int y = 10;
DoSomething(y);

// These aren't helping at all!
x = null;
sb = null;

// Assume that x and sb aren't used here

La seule fois où cela peut-être vaut la peine de définir une variable locale sur null, c'est lorsque vous êtes dans une boucle, et certaines branches de la boucle doivent utiliser la variable mais vous sachez que vous avez atteint un point où vous ne le faites pas. Par exemple:

SomeObject foo = new SomeObject();

for (int i=0; i < 100000; i++)
{
    if (i == 5)
    {
        foo.DoSomething();
        // We're not going to need it again, but the JIT
        // wouldn't spot that
        foo = null;
    }
    else
    {
        // Some other code 
    }
}

Implémentation d'IDisposable/finaliseurs

Alors, vos propres types devraient-ils implémenter des finaliseurs? Certainement pas. Si vous seulement indirectement détenez des ressources non managées (par exemple, vous avez un FileStream en tant que variable membre), alors l'ajout de votre propre finaliseur n'aidera pas: le flux sera presque certainement éligible pour la collecte des ordures lorsque votre objet l'est, vous pouvez donc simplement compter sur FileStream ayant un finaliseur (si nécessaire - il peut faire référence à autre chose, etc.). Si vous voulez détenir une ressource non gérée "presque" directement, SafeHandle est votre ami - cela prend un peu de temps pour commencer, mais cela signifie que vous presqueplus besoin d'écrire à nouveau un finaliseur . Vous ne devriez généralement avoir besoin d'un finaliseur que si vous avez une poignée vraiment directe sur une ressource (un IntPtr) et vous devriez chercher à passer à SafeHandle dès que vous le pouvez. (Il y a deux liens - lisez les deux idéalement.)

Joe Duffy a un très long ensemble de directives concernant les finaliseurs et IDisposable (co-écrit avec beaucoup de gens intelligents) qui valent la peine d'être lu. Il vaut la peine d'être conscient que si vous scellez vos classes, cela rend la vie beaucoup plus facile: le modèle de substitution de Dispose pour appeler une nouvelle méthode virtuelle Dispose(bool) etc n'est pertinent que lorsque votre classe est conçue pour l'héritage.

Cela a été un peu errant, mais veuillez demander des éclaircissements où vous en voulez :)

205
Jon Skeet

Lorsque vous supprimez un objet, les ressources sont libérées. Lorsque vous affectez null à une variable, vous modifiez simplement une référence.

myclass = null;

Après avoir exécuté cela, l'objet auquel ma classe faisait référence existe toujours et continuera jusqu'à ce que le GC se déplace pour le nettoyer. Si Dispose est explicitement appelé ou s'il se trouve dans un bloc using, toutes les ressources seront libérées dès que possible.

22
recursive

Les deux opérations n'ont pas grand-chose à voir l'une avec l'autre. Lorsque vous définissez une référence sur null, cela fait simplement cela. Cela n'affecte pas en soi la classe qui a été référencée du tout. Votre variable ne pointe tout simplement plus vers l'objet auquel elle était auparavant, mais l'objet lui-même est inchangé.

Lorsque vous appelez Dispose (), il s'agit d'un appel de méthode sur l'objet lui-même. Tout ce que fait la méthode Dispose, se fait maintenant sur l'objet. Mais cela n'affecte pas votre référence à l'objet.

Le seul domaine de chevauchement est que quand il n'y a plus de références à un objet, il éventuellement récupérera les ordures. Et si la classe implémente l'interface IDisposable, Dispose () sera appelé sur l'objet avant qu'il ne soit récupéré.

Mais cela ne se produira pas immédiatement après avoir défini votre référence sur null, pour deux raisons. Premièrement, d'autres références peuvent exister, donc il ne sera pas du tout récupéré pour le moment, et deuxièmement, même si c'était la dernière référence, il est donc prêt à être récupéré, rien ne se passera jusqu'à ce que le garbage collector décide de supprimer L'object.

Appeler Dispose () sur un objet ne "tue" pas l'objet en aucune façon. Il est couramment utilisé pour nettoyer afin que l'objet can soit supprimé en toute sécurité par la suite, mais au final, Dispose n'a rien de magique, c'est juste une méthode de classe.

6
jalf