Définir un object sur null vs Dispose ()

Je suis fasciné par la façon dont fonctionnent le CLR et le GC (je travaille à étendre 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 en sorte que MyClass implémente IDisposable et un destructeur et appelle Dispose ()?

De plus, si j’ai un bloc de code avec une instruction using (par exemple ci-dessous), si je traverse le code et quitte le bloc using, l’object est-il éliminé ou lorsqu’une récupération de mémoire se produit? Que se passerait-il si j’appelais Dispose () dans le bloc d’utilisation quand même?

 using (MyDisposableObj mydispobj = new MyDisposableObj()) { } 

Les classes de stream (par exemple BinaryWriter) ont une méthode Finalize? Pourquoi voudrais-je l’utiliser?

    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 que je vais aborder dans une minute.

    Dispose , la récupération de la mémoire et la finalisation

    Lorsque vous écrivez une instruction using , c’est simplement du sucre syntaxique pour un bloc try / finally de sorte que Dispose soit appelé même si le code dans le corps de l’instruction using émet une exception. Cela ne signifie pas que l’object est vide à la fin du bloc.

    L’élimination concerne les ressources non gérées ( ressources non mémoire). Celles-ci peuvent être des descripteurs d’interface utilisateur, des connexions réseau, des descripteurs de fichiers, etc. Ces ressources sont limitées. Vous souhaitez donc généralement les libérer dès que possible. Vous devez implémenter IDisposable chaque fois que votre type “possède” une ressource non gérée, directement (généralement via un IntPtr ) ou indirectement (par exemple via un Stream , un SqlConnection etc.).

    La collecte des ordures ne concerne que la mémoire – avec un petit changement. Le garbage collector est capable de trouver des objects qui ne peuvent plus être référencés et de les libérer. Cependant, il ne recherche pas les ordures tout le temps, mais seulement quand il détecte que cela est nécessaire (par exemple, si une “génération” du tas manque de mémoire).

    La torsion est la finalisation . Le ramasse-miettes garde une liste d’objects qui ne sont plus joignables, mais qui ont un finaliseur (écrit en tant que ~Foo() en C #), quelque peu déroutant – ils ne ressemblent en rien aux destructeurs C ++). Il exécute les finaliseurs sur ces objects, 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. Donc, si vous ouvrez un FileStream mais oubliez d’appeler Dispose ou Close , le finaliseur publiera éventuellement le descripteur de fichier sous-jacent pour vous. Dans un programme bien écrit, les finalisateurs ne devraient presque jamais tirer à mon avis.

    Définition d’une variable sur null

    Un petit point sur la définition d’une variable à null – cela n’est presque jamais nécessaire pour le nettoyage de la mémoire. Vous voudrez peut-être parfois le faire s’il s’agit d’une variable membre, bien que dans mon expérience, il soit rare qu’une “partie” d’un object 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:

     SsortingngBuilder sb = new SsortingngBuilder(); sb.Append("Foo"); ssortingng x = sb.ToSsortingng(); // The ssortingng and SsortingngBuilder 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ù il peut être utile de définir une variable locale sur null c’est lorsque vous êtes en boucle et que certaines twigs de la boucle doivent utiliser la variable, mais vous savez 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émenter IDisposable / finalizers

    Donc, vos propres types devraient-ils implémenter des finaliseurs? Presque certainement pas. Si vous ne détenez qu'indirectement des ressources non managées (par exemple, vous avez une variable FileStream tant que variable membre), l'ajout de votre propre finaliseur ne sera d'aucune aide: le stream sera certainement éligible pour le nettoyage de la mémoire. sur FileStream ayant un finaliseur (si nécessaire - il peut faire référence à autre chose, etc.). Si vous souhaitez conserver 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 n'aurez presque plus besoin d'écrire un finalizer . Vous ne devriez généralement avoir besoin que d'un finaliseur si vous avez un IntPtr très direct sur une ressource (un IntPtr ) et que vous devriez chercher à passer à SafeHandle dès que possible. (Il y a deux liens - lisez les deux, idéalement.)

    Joe Duffy a une très longue série de directives sur les finaliseurs et les IDisposables (co-écrits avec beaucoup de gens intelligents) qui méritent d'être lus. Cela vaut la peine de savoir 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'inheritance.

    Cela a été un peu une randonnée, mais s'il vous plaît demander des éclaircissements où vous en voudriez 🙂

    Lorsque vous disposez d’un object, les ressources sont libérées. Lorsque vous atsortingbuez la valeur null à une variable, vous modifiez simplement une référence.

     myclass = null; 

    Une fois que vous avez exécuté ceci, l’object auquel ma classe se référait existe toujours et continuera jusqu’à ce que le GC parvienne à le nettoyer. Si Dispose est explicitement appelé ou s’il est utilisé dans un bloc, toutes les ressources seront libérées dès que possible.

    Les deux opérations n’ont pas grand chose à faire les unes avec les autres. Lorsque vous définissez une référence à null, cela le fait simplement. En soi, cela n’affecte pas du tout la classe référencée. Votre variable ne pointe plus simplement vers l’object auquel elle était habituée, mais l’object lui-même n’a pas changé.

    Lorsque vous appelez Dispose (), c’est un appel de méthode sur l’object lui-même. Quelle que soit la méthode Dispose, elle est maintenant effectuée sur l’object. Mais cela n’affecte pas votre référence à l’object.

    La seule zone de chevauchement est que lorsqu’il n’y a plus de références à un object, il sera éventuellement récupéré. Et si la classe implémente l’interface IDisposable, alors Dispose () sera appelée sur l’object 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. D’abord, d’autres références peuvent exister, donc il n’y aura pas encore de récupération de mémoire, et deuxièmement, même s’il s’agissait de la dernière référence, il est maintenant prêt à être récupéré. L’object.

    L’appel de Dispose () sur un object ne “tue” pas l’object de quelque manière que ce soit. Il est couramment utilisé pour nettoyer afin que l’object puisse être supprimé en toute sécurité par la suite, mais finalement, il n’y a rien de magique à propos de Dispose, c’est juste une méthode de classe.