1595 votes

La bonne utilisation de l'interface IDisposable

Je sais à partir de la lecture de la documentation MSDN que le "primaire" de l'utilisation de l'interface IDisposable est de nettoyer les ressources non managées.

Pour moi, "non géré" signifie des choses comme des connexions de base de données, des sockets, des poignées de fenêtre, etc. Mais, j'ai vu du code, où la méthode dispose est mis en œuvre pour gratuit géré ressources, qui semble redondant pour moi, depuis le garbage collector devrait prendre soin de cela pour vous.

Par exemple:

public class MyCollection : IDisposable
{
    private List<String> _theList = new List<String>();
    private Dictionary<String, Point> _theDict = new Dictionary<String, Point>();

    // Die, clear it up! (free unmanaged resources)
    public void Dispose()
    {
        _theList.clear();
        _theDict.clear();
        _theList = null;
        _theDict = null;
    }

Ma question est, est-ce faire le garbage collector libérer la mémoire utilisée par MyCollection plus vite qu'il le ferait normalement?

edit: Donc, beaucoup de gens ont posté quelques bons exemples de l'utilisation de IDisposable pour nettoyer les ressources non managées comme des connexions de base de données et d'images. Mais supposons que _theList dans le code ci-dessus figurant un million de chaînes, et que vous vouliez pour libérer de la mémoire maintenant, plutôt que d'attendre que le garbage collector. Serait le code ci-dessus le réaliser?

2527voto

Ian Boyd Points 50743

Le point de Céder est de libérer des ressources non managées. Il doit être fait à un certain point, sinon ils ne seront jamais nettoyé. Le garbage collector ne sais pas comment appeler DeleteHandle() sur une variable de type IntPtr, il ne sait pas si elle doit ou non appel DeleteHandle().

Remarque: Ce qui est une ressource non managée? Si vous l'avez trouvé dans le Microsoft .NET Cadre: c'est réussi. Si vous êtes allé farfouillé MSDN vous-même, c'est non managé. Tout ce que vous avez utilisé P/Invoke appels à obtenir à l'extérieur de la confortable monde de tout disponible pour vous dans le .NET Framwork ne sont pas gérés, et vous êtes maintenant responsable du nettoyage.

L'objet que vous avez créé doit exposer certains de la méthode, que le monde extérieur peut faire appel, afin de nettoyer les ressources non managées. Il y a même un nom normalisé pour cette méthode:

public void Dispose()

Il y a même une interface créée, IDisposable, qui vient d'une méthode:

public interface IDisposable
{
   void Dispose()
}

Afin de vous rendre votre objet d'exposer l' IDisposable interface, et de cette façon vous promets que vous avez écrit que la seule méthode pour nettoyer vos ressources non managées:

public void Dispose()
{
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);
}

Et vous avez terminé. Sauf que vous pouvez faire mieux.


Que faire si votre objet a alloué une 250MB Système.De dessin.Bitmap (c'est à dire l' .NET géré classe Bitmap) comme une sorte de mémoire tampon de trame? Bien sûr, cela est géré .NET de l'objet, et le garbage collector gratuit. Mais voulez-vous vraiment quitter 250 MO de mémoire juste assis là en attente pour le garbage collector pour finalement venir le long et de les libérer? Que faire si il y a une base de données ouverte de connexion? Certes, nous ne voulons pas que la connexion séance ouverte, en attente pour le GC pour finaliser l'objet.

Si l'utilisateur a appelé Dispose() (ce qui signifie qu'ils ne sont plus du plan d'utilisation de l'objet), pourquoi ne pas se débarrasser de ceux gaspillage des bitmaps et des connexions de base de données?

Alors maintenant, nous allons:

  • se débarrasser des ressources non managées (parce que nous avons), et
  • se débarrasser de la gestion des ressources (parce que nous voulons être utile)

Donc, nous allons mettre à jour notre Dispose() méthode pour se débarrasser de ces objets gérés:

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose();
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose();
      this.frameBufferImage = null;
   }
}

Et tous est bon, sauf que vous pouvez faire mieux!


Que faire si la personne a oublié d'appeler Dispose() sur votre objet? Alors qu'ils seraient à la fuite de certains non géré ressources!

Remarque: Ils ne fuit pas géré les ressources, parce que finalement le garbage collector se déroulera, sur un thread d'arrière-plan, et de libérer la associée de la mémoire inutilisée objets. Il s'agit de votre objet, et tout des objets gérés que vous utilisez (par exemple, l'image et le DbConnection).

Si la personne a oublié d'appeler Dispose(), nous pouvons encore sauver leur bacon! Nous avons encore un moyen de l'appeler pour eux: lorsque le garbage collector, enfin, se déplace à libérer (c'est à dire la finalisation) de notre objet.

Remarque: Le garbage collector sera finalement libre de tous les objets gérés. Lorsqu'il le fait, il appelle l' Finalize méthode sur l'objet. Le GC ne savent pas, ou de soins, sur votre Disposerde la méthode. C'était juste un nom que nous avons choisi pour une méthode que nous appelons lorsque l'on veut obtenir débarrasser de non géré choses.

La destruction de notre objet par le Garbage collector est le parfait moment pour libérer ces satanés des ressources non managées. Nous faisons cela en remplaçant l' Finalize() méthode.

Remarque: En C#, vous n'avez pas explicitement remplacer l' Finalize() méthode. Vous écrivez une méthode qui ressemble à un destructeur C++ , et le compilateur prend que pour être votre mise en œuvre de l' Finalize() méthode:

~MyObject()
{
    //we're being finalized (i.e. destroyed), call Dispose in case the user forgot to
    Dispose(); //<--Warning: subtle bug! Keep reading!
}

Mais il y a un bug dans le code. Vous voyez, le garbage collector s'exécute sur un thread d'arrière-plan, vous ne savez pas l'ordre dans lequel les deux objets sont détruits. Il est tout à fait possible que, dans votre Dispose() code, l' géré objet que vous essayez de vous débarrasser de (parce que tu voulais être utile) n'est plus là:

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose(); <-- crash, GC already destroyed it
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose(); <-- crash, GC already destroyed it
      this.frameBufferImage = null;
   }
}

Donc, ce que vous avez besoin est une façon pour Finalize() de raconter Dispose() qu'il devrait pas toucher tout géré les ressources (parce qu'ils pourraient ne pas être là plus), tout en libérant des ressources non managées.

La configuration standard pour ce faire est d'avoir Finalize() et Dispose() à la fois appel à un tiers(!) méthode; où l'on passe un Booléen disant: si vous appelez à partir d' Dispose() (par opposition à l' Finalize()), ce qui signifie qu'il est sûr de gratuit ressources gérées.

Cette interne méthode pourrait être donné quelques arbitraire nom comme "CoreDispose", ou "MyInternalDispose", mais c'est la tradition à l'appellent Dispose(Boolean):

protected void Dispose(Boolean disposing)

Mais un des plus utiles nom du paramètre peut être:

protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);

   //Free managed resources too, but only if I'm being called from Dispose
   //(If I'm being called from Finalize then the objects might not exist
   //anymore
   if (itIsSafeToAlsoFreeManagedObjects)  
   {    
      if (this.databaseConnection != null)
      {
         this.databaseConnection.Dispose();
         this.databaseConnection = null;
      }
      if (this.frameBufferImage != null)
      {
         this.frameBufferImage.Dispose();
         this.frameBufferImage = null;
      }
   }
}

Et vous changez la mise en œuvre de l' IDisposable.Dipose() méthode pour:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
}

et votre outil de finalisation:

~MyObject()
{
   Dispose(false); //I am *not* calling you from Dispose, it's *not* safe
}

Remarque: Si votre objet descend d'une objet qui implémente la Jeter, puis n'oubliez pas d'appeler leur base de Disposer de la méthode lorsque vous remplacez les Éliminer:

public Dispose()
{
    try
    {
        Dispose(true); //true: safe to free managed resources
    }
    finally
    {
        base.Dispose();
    }
}

Et tous est bon, sauf que vous pouvez faire mieux!


Si l'utilisateur appelle Dispose() sur votre objet, puis tout a été nettoyé. Plus tard, lorsque le garbage collector vient le long et les appels de Finaliser, il sera alors appel Dispose de nouveau.

Ce n'est pas seulement inutile, mais si votre objet a ordure des références à des objets que vous avez déjà éliminés depuis le dernier appel à l' Dispose(), que vous allez essayer de les disposer à nouveau!

Vous remarquerez que dans mon code j'ai eu soin de supprimer les références à des objets que j'ai cédé, je n'ai pas essayer d'appeler Disposer à bord d'une jonque de référence de l'objet. Mais cela n'empêche pas un bug subtil rampant dans.

Lorsque l'utilisateur appelle Dispose(): la poignée gdiCursorBitmapStreamFileHandle est détruit. Plus tard, lorsque le garbage collector est exécuté, il va tenter de détruire le même handle de nouveau.

protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle); <--double destroy 
   ...
}

La façon de résoudre ce problème est de dire que le garbage collector qu'il n'a pas besoin de s'embêter à la finalisation de l'objet de ses ressources ont déjà été nettoyés, et pas plus de travail est nécessaire. Vous faites cela en appelant GC.SuppressFinalize() dans la Dispose() méthode:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
   GC.SuppressFinalize(this); //Hey, GC: don't bother calling finalize later
}

Maintenant que l'utilisateur a appelé Dispose(), nous avons:

  • libéré des ressources non managées
  • libéré ressources gérées

Il n'y a pas de point dans le GC de l'exécution de l'outil de finalisation, tout est pris en charge.


Pour répondre à votre question initiale: Pourquoi ne pas libérer de la mémoire maintenant, plutôt que lorsque le GC décide de le faire? J'ai un logiciel de reconnaissance faciale qui besoin de se débarrasser de 530 MO d'images internes maintenant, car elles ne sont plus nécessaires. Quand on n'a pas: la machine broie à une permutation de s'arrêter.

Bonus De Lecture

Pour tout ceux qui aime le style de cette réponse (en expliquant le pourquoi, de sorte que le comment devient évident), je vous conseille de lire le Chapitre Un de Ne de la Boîte de COM Essentiel:

En 35 pages, il explique les problèmes de l'utilisation des objets binaires, et invente COM devant vos yeux. Une fois que vous savez le pourquoi de COM, le restant de 300 pages sont évidentes, et juste détail de Microsoft de la mise en œuvre.

Je pense que chaque programmeur qui a déjà fait affaire avec des objets ou des COM devrait, à tout le moins, de lire le premier chapitre. C'est la meilleure explication de quoi que ce soit jamais.

59voto

yfeldblum Points 42613

IDisposable est souvent utilisé pour exploiter l' using déclaration et de profiter d'un moyen facile de le faire déterministe de nettoyage d'objets gérés.

public class LoggingContext : IDisposable {
    public Finicky(string name) {
        Log.Write("Entering Log Context {0}", name);
        Log.Indent();
    }
    public void Dispose() {
        Log.Outdent();
    }

    public static void Main() {
        Log.Write("Some initial stuff.");
        try {
            using(new LoggingContext()) {
                Log.Write("Some stuff inside the context.");
                throw new Exception();
            }
        } catch {
            Log.Write("Man, that was a heavy exception caught from inside a child logging context!");
        } finally {
            Log.Write("Some final stuff.");
        }
    }
}

42voto

Scott Dorman Points 25000

Le but du modèle dispose est de fournir un mécanisme pour nettoyer à la fois réussi et les ressources non managées et lorsque cela se produit dépend de la façon dont la méthode dispose est appelé. Dans votre exemple, l'utilisation de Jeter n'est pas en train de faire tout ce qui est lié à disposer, depuis la compensation d'une liste n'a pas d'impact sur la collection en cours de cession. De même, les appels à définir les variables à null également ne pas avoir d'impact sur la GC.

Vous pouvez prendre un coup d'oeil à cet article pour plus de détails sur la façon de mettre en œuvre le modèle dispose, mais fondamentalement, ça ressemble à ça:

public class SimpleCleanup : IDisposable
{
    // some fields that require cleanup
    private SafeHandle handle;
    private bool disposed = false; // to detect redundant calls

    public SimpleCleanup()
    {
        this.handle = /*...*/;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Dispose managed resources.
                if (handle != null)
                {
                    handle.Dispose();
                }
            }

            // Dispose unmanaged managed resources.

            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

La méthode qui est le plus important ici est l'dispose(bool), qui en réalité s'exécute dans deux cas différents:

  • l'élimination == true: la méthode a été appelée directement ou indirectement par un code d'utilisateur. Gérés et non gérés, les ressources peuvent être éliminés.
  • l'élimination == false: la méthode a été appelée par le moteur d'exécution de l'intérieur de l'outil de finalisation, et vous ne devriez pas faire référence à d'autres objets. Seulement les ressources non managées peuvent être éliminés.

Le problème avec simplement laisser le GC prendre soin de faire le nettoyage, c'est que vous n'avez pas de réel contrôle sur le moment où le GC exécuter un cycle de collecte de données (vous pouvez appeler GC.Collect(), mais vous ne devriez vraiment pas), alors que les ressources peuvent rester plus longtemps que nécessaire. Rappelez-vous, en appelant Dispose() n'est pas réellement de causer un cycle de collecte ou de quelque façon que provoquer la GC pour les collecter, de les gratuit de l'objet; il fournit simplement le moyen de deterministicly nettoyage des ressources utilisées et de dire la GC que ce nettoyage a déjà été effectué.

Le point de l'ensemble de IDisposable et le modèle dispose n'est pas immédiatement libérer de la mémoire. Le seul moment où un appel à Disposer va en fait de même avoir une chance de faire immédiatement la libération de la mémoire, c'est quand il est de la manipulation de l'élimination == false scénario et la manipulation des ressources non managées. Pour le code managé, la mémoire n'est pas réellement être récupéré jusqu'à ce que le GC exécute un cycle de collecte, vous vraiment n'avez aucun contrôle sur (autres que l'appel d'GC.Collect(), qui comme je l'ai déjà mentionné, c'est pas une bonne idée).

Votre scénario n'est pas vraiment valable depuis les cordes .NET ne pas utiliser n'importe quel unamanged ressources et de ne pas mettre en œuvre IDisposable, il n'y a aucun moyen de les forcer à être "nettoyé."

18voto

Daniel Earwicker Points 63298

Il ne devrait plus les appels à un objet de méthodes d'Aliéner a été appelée sur elle (même si un objet doit tolérer appelle en outre à Disposition). Par conséquent, l'exemple de la question est stupide. Si Disposer est appelée, l'objet lui-même peut être mis au rebut. Ainsi l'utilisateur doit juste jeter toutes les références à cet objet entier (mis à null) et de tous les objets liés à l'interne il sera automatiquement nettoyé.

Comme pour la question générale sur les sites gérés et non gérés, et la discussion dans d'autres réponses, je pense que la réponse à cette question doit commencer par une définition d'une ressource non managée.

Ce qui se résume à est qu'il y a une fonction que vous pouvez appeler pour mettre le système dans un état, et il y a une autre fonction que vous pouvez appeler pour le ramener à sortir de cet état. Maintenant, dans le typique exemple, le premier peut-être une fonction qui renvoie un descripteur de fichier, et le second pourrait être un appel à l' CloseHandle.

Mais - et c'est la clé, ils pourraient être n'importe quelle paire de fonctions. On construit un etat, l'autre l'arrache. Si l'état a été construit, mais pas déchiré vers le bas encore, puis une instance de la ressource existe. Vous devez prendre des dispositions pour le démontage pour arriver au bon moment - la ressource n'est pas géré par le CLR. La seule automatiquement géré type de ressource est la mémoire. Il en existe deux types: le GC, et la pile. Types de valeur sont gérés par la pile (ou par baladait à l'intérieur de types de référence), et les types de référence sont gérés par le GC.

Ces fonctions peuvent provoquer des modifications de l'état qui peuvent être librement entrelacés, ou peut-être besoin d'être parfaitement imbriqués. Les changements d'état peuvent être thread-safe, ou ils ne pourraient pas.

Regardez l'exemple dans la Justice de la question. Les modifications dans le fichier Journal de l'indentation doit être parfaitement imbriqués, ou tout va mal. En outre, ils sont peu susceptibles d'être thread-safe.

Il est possible de faire du stop avec le garbage collector pour obtenir votre des ressources non managées nettoyé. Mais seulement si le changement d'état des fonctions sont thread-safe et deux états peuvent avoir des durées de vie qui se chevauchent en aucune façon. Alors que la Justice l'exemple d'une ressource ne doit PAS avoir un finaliseur! Il n'aurait tout simplement pas aider tout le monde.

Pour ces types de ressources, vous pouvez juste mettre en oeuvre IDisposable, sans un finaliseur. Le finalizer est absolument facultatif - il doit l'être. C'est l'estomper ou même pas mentionné dans de nombreux ouvrages.

Ensuite, vous devez utiliser l' using déclaration pour avoir une chance de s'assurer qu' Dispose est appelé. C'est pratiquement comme se baladait avec la pile (donc comme finalizer est à la GC, using est à la pile).

La partie manquante est que vous avez à écrire manuellement Éliminer et de faire appel sur vos champs et de votre classe de base. C++/CLI, les programmeurs ne pas avoir à le faire. Le compilateur écrit pour eux dans la plupart des cas.

Il y a une alternative, que je préfère pour les états qui s'emboîtent parfaitement et ne sont pas thread-safe (en dehors de toute autre chose, en évitant IDisposable vous épargne le problème d'avoir une dispute avec quelqu'un qui ne peut pas résister à l'ajout d'un finaliseur à chaque classe qui implémente IDisposable).

Au lieu d'écrire une classe, vous écrivez une fonction. La fonction accepte un délégué pour rappel:

public static void Indented(this Log log, Action action)
{
    log.Indent();
    try
    {
        action();
    }
    finally
    {
        log.Outdent();
    }
}

Et puis un exemple simple serait:

Log.Write("Message at the top");
Log.Indented(() =>
{
    Log.Write("And this is indented");

    Log.Indented(() =>
    {
        Log.Write("This is even more indented");
    });
});
Log.Write("Back at the outermost level again");

Le lambda étant passé en sert comme d'un bloc de code, donc c'est comme vous faire votre propre structure de contrôle pour servir le même but que using, sauf que vous n'avez plus aucun danger de l'appelant en abuser. Il n'ya aucun moyen qu'ils peuvent échouer pour nettoyer la ressource.

Cette technique est moins utile si la ressource est le genre qui peut avoir cumul des durées de vie, parce qu'alors, vous voulez être en mesure de construire des ressources A, puis B de ressource, puis de les tuer ressource et puis plus tard, tuer des ressources B. Vous ne pouvez pas faire cela si vous avez forcé à l'utilisateur de parfaitement nid comme ça. Mais alors vous devez utiliser IDisposable (mais toujours sans un finaliseur, sauf si vous avez mis en œuvre threadsafety, qui n'est pas gratuit).

15voto

olli Points 1945

Scénarios-je faire usage de IDisposable: nettoyer les ressources non managées, désabonnez-vous pour des événements, des liens étroits

Le langage que j'utilise pour la mise en œuvre de IDisposable (pas thread-safe):

class MyClass : IDisposable {
    // ...

    #region IDisposable Members and Helpers
    private bool disposed = false;

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing) {
        if (!this.disposed) {
            if (disposing) {
                // cleanup code goes here
            }
            disposed = true;
        }
    }

    ~MyClass() {
        Dispose(false);
    }
    #endregion
}

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