Définir un objet sur null vs Dispose ()


108

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, etc.).

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

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

Ou, en faisant MyClass implémenter 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 éliminé à ce moment-là 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?

Réponses:


210

Il est important de séparer l'élimination de la collecte des ordures. Ce sont des choses complètement séparées, avec un point commun sur lequel je reviendrai dans une minute.

Dispose, ramassage des ordures et finalisation

Lorsque vous écrivez une usinginstruction, c'est simplement du sucre syntaxique pour un bloc try / finally, qui Disposeest appelé même si le code dans le corps de l' usinginstruction lève une exception. Cela ne signifie pas que l'objet est ramassé à la fin du bloc.

L'élimination concerne les ressources non gérées ( ressources non-mémoire). Il peut s'agir de descripteurs d'interface utilisateur, de connexions réseau, de descripteurs de fichiers, etc. Ce sont des ressources limitées, vous voulez donc généralement les libérer dès que vous le pouvez. Vous devez implémenter IDisposablechaque fois que votre type "possède" une ressource non gérée, soit directement (généralement via un IntPtr) ou indirectement (par exemple via a Stream, a, SqlConnectionetc.).

Le ramassage des ordures lui-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 de déchets tout le temps - seulement quand il détecte qu'il en a besoin (par exemple, si une "génération" du tas manque de mémoire).

La torsion est la 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 déroutant - 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 s'en débarrasser de manière ordonnée. Ainsi, si vous ouvrez un FileStreammais oubliez d'appeler Disposeou Close, le finaliseur finira par libérer 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 voudrez peut-être parfois le faire s'il s'agit d'une variable membre, bien que d'après 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'allez plus utiliser 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

Le seul moment où il peut être intéressant de définir une variable locale nullest lorsque vous êtes dans une boucle, et certaines branches de la boucle doivent utiliser la variable, mais vous savez que vous avez atteint un point auquel vous ne l'avez 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 / finalizers

Alors, vos propres types devraient-ils implémenter des finaliseurs? Presque certainement pas. Si vous ne détenez qu'indirectement des ressources non gérées (par exemple, vous avez une FileStreamvariable en tant que membre), alors l'ajout de votre propre finaliseur n'aidera pas: le flux sera presque certainement éligible pour le ramasse-miettes lorsque votre objet est, donc vous pouvez simplement compter sur FileStreamavoir un finaliseur (si nécessaire - il peut faire référence à autre chose, etc.). Si vous voulez conserver une ressource non gérée «presque» directement, SafeHandlec'est votre ami - cela prend un peu de temps pour y aller, mais cela signifie que vous n'aurez presque plus jamais besoin d'écrire 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 (an IntPtr) et que vous devriez chercher à passer àSafeHandledès que vous le pouvez. (Il y a deux liens ici - 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 lus. Il vaut la peine de savoir que si vous scellez vos classes, cela vous facilite beaucoup la vie: le modèle de substitution Disposepour appeler une nouvelle Dispose(bool)méthode virtuelle , etc. n'est pertinent que lorsque votre classe est conçue pour l'héritage.

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


Re "La seule fois où il peut valoir la peine de définir une variable locale sur null" - peut-être aussi certains des scénarios de "capture" les plus épineux (captures multiples de la même variable) - mais cela ne vaut peut-être pas la peine de compliquer le message! +1 ...
Marc Gravell

@Marc: C'est vrai - je n'avais même pas pensé aux variables capturées. Hmm. Ouais, je pense que je vais laisser ça tranquille;)
Jon Skeet

pourriez-vous s'il vous plaît dire que ce qui se passera lorsque vous définissez "foo = null" dans votre extrait de code ci-dessus? Autant que je sache, cette ligne efface uniquement la valeur d'une variable pointant vers l'objet foo dans le tas géré? la question est donc de savoir ce qui va arriver à l'objet foo là-bas? ne devrions-nous pas appeler disposer de cela?
odiseh

@odiseh: Si l'objet était jetable, alors oui - vous devriez le jeter. Cette section de la réponse ne traitait que de la collecte des ordures, qui est entièrement distincte.
Jon Skeet

1
Je cherchais une clarification sur certaines préoccupations IDisposable, alors j'ai cherché sur Google "IDisposable Skeet" et j'ai trouvé ceci. Génial! : D
Maciej Wozniak

22

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;

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


7
Il peut ne pas exister encore après l'exécution de cette ligne - il peut avoir été ramassé avant cette ligne. Le JIT est intelligent - rendant des lignes comme celle-ci presque toujours hors de propos.
Jon Skeet

6
La valeur null peut signifier que les ressources détenues par l'objet ne sont jamais libérées. Le GC ne dispose pas, il ne fait que finaliser, donc si l'objet contient directement des ressources non managées et que son finaliseur ne dispose pas (ou s'il n'a pas de finaliseur), ces ressources vont fuir. Quelque chose dont il faut être conscient.
LukeH

6

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, il le fait simplement. Cela n'affecte pas du tout la classe référencée. Votre variable ne pointe tout simplement plus vers l'objet auquel elle avait l'habitude, mais l'objet lui-même est inchangé.

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

Le seul domaine de chevauchement est que lorsqu'il n'y a plus de références à un objet, il finira par être récupéré. Et si la classe implémente l'interface IDisposable, Dispose () sera appelée 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 ramassé du tout, et deuxièmement, même si c'était la dernière référence, donc il est maintenant prêt à être ramassé, rien ne se passera jusqu'à ce que le ramasse-miettes décide de supprimer L'object.

L'appel de Dispose () sur un objet ne «tue» en aucun cas l'objet. Il est couramment utilisé pour nettoyer afin que l'objet puisse être supprimé en toute sécurité par la suite, mais finalement, il n'y a rien de magique à Dispose, c'est juste une méthode de classe.


Je pense que cette réponse complimente ou est un détail à la réponse de «récursif».
dance2die
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.