Qui devrait appeler pour disposer des objets Idisposables lorsqu'ils sont transférés dans un autre objet?

y a-t-il des lignes directrices ou des pratiques exemplaires concernant qui devrait appeler Dispose() sur les objets jetables lorsqu'ils ont été transmis à des méthodes ou à un constructeur d'un autre objet?

Voici quelques exemples de ce que je veux dire.

IDisposable objet est passé dans une méthode (faut-il en disposer une fois que c'est fait?):

public void DoStuff(IDisposable disposableObj)
{
    // Do something with disposableObj
    CalculateSomething(disposableObj)

    disposableObj.Dispose();
}

objet IDisposable est passé dans une méthode et une référence est conservée (si elle en disposer quand MyClass est éliminé?):

public class MyClass : IDisposable
{
    private IDisposable _disposableObj = null;

    public void DoStuff(IDisposable disposableObj)
    {
        _disposableObj = disposableObj;
    }

    public void Dispose()
    {
        _disposableObj.Dispose();
    }
}

je pense actuellement que dans le premier exemple le appelant de DoStuff() devrait disposer de l'objet comme il a probablement créé l'objet. Mais dans le deuxième exemple, il semble que MyClass devrait disposer de l'objet comme il garde une référence à elle. Le problème avec cela est que la classe appelante pourrait ne pas savoir MyClass a gardé une référence et pourrait donc décider pour disposer de l'objet avant de MyClass a fini de l'utiliser. Existe-il des règles standard pour ce genre de scénario? S'il y en a, diffèrent-ils lorsque l'objet jetable est passé dans un constructeur?

56
demandé sur James Bateson 2010-11-03 13:13:15

5 réponses

en règle générale, si vous avez créé l'objet (ou en avez acquis la propriété), vous devez en disposer. Cela signifie que si vous recevez un objet jetable comme paramètre dans une méthode ou un constructeur, vous ne devez généralement pas l'éliminer.

notez que certaines classes dans le .net framework do éliminent les objets qu'ils ont reçus comme paramètres. Par exemple, la disposition d'un StreamReader permet également de disposer du Stream sous-jacent .

35
répondu Mark Byers 2010-11-03 10:21:48

P.S.: j'ai posté une nouvelle réponse (contenant un ensemble simple de règles qui devrait appeler Dispose , et comment concevoir une API qui traite des objets IDisposable ). Bien que la présente réponse contienne de précieuses idées, j'en suis venu à croire que sa principale suggestion ne fonctionnera souvent pas dans la pratique: cacher des objets IDisposable dans des objets "à grain grossier" signifie souvent que ceux-ci doivent devenir IDisposable on finit donc là où on a commencé, et le problème demeure.


y a-t-il des lignes directrices ou des pratiques exemplaires concernant qui devrait appeler Dispose() sur les objets jetables lorsqu'ils ont été transmis à des méthodes ou à un constructeur d'un autre objet?

brève réponse:

Oui, il y a beaucoup conseils sur ce sujet, et le mieux que je sais est Eric Evans ' concept de agrégats dans domaine-Driven Design . (En termes simples, l'idée de base appliquée à IDisposable est la suivante: encapsuler le IDisposable dans un composant plus grossier de sorte qu'il n'est pas vu par l'extérieur et n'est jamais passé au consommateur du composant.)

de plus, l'idée que le créateur d'un IDisposable objet doit également être en charge de l'élimination il est trop restrictif et souvent ne fonctionnera pas dans la pratique.

le reste de ma réponse est plus détaillé sur les deux points, dans le même ordre. Je terminerai ma réponse en donnant quelques conseils sur d'autres documents qui se rapportent au même sujet.

réponse plus longue - sur quoi porte cette question en termes plus larges:

Conseil sur ce sujet n'est généralement pas spécifique à IDisposable . Chaque fois que les gens parlent de la durée de vie de l'objet et de la propriété, ils font référence à la même question (mais en termes plus généraux).

pourquoi ce sujet ne se pose-t-il presque jamais dans l'écosystème. net? Parce que l'environnement runtime (CLR) de. Net effectue la collecte automatique des ordures, qui fait tout le travail pour vous: si vous n'avez plus besoin d'un objet, vous pouvez tout simplement l'oublier et le collecteur d'ordures sera éventuellement récupérer sa mémoire.

Pourquoi donc la question se pose-t-elle avec des objets IDisposable ? Parce que IDisposable est tout au sujet du contrôle explicite et déterministe de la durée de vie d'une (souvent clairsemée ou chère) ressource: IDisposable objets sont censés être libérés dès qu'ils ne sont plus nécessaires - et la garantie indéterminée du collecteur d'ordures ("je vais éventuellement réclamer le mémoire utilisé par vous!") simplement n'est pas assez bon.

votre question, reformulée dans les Termes plus larges de la durée de vie de l'objet et de la propriété:

quel objet O devrait être responsable de la fin de la durée de vie d'un objet (jetable) D , qui passe aussi aux objets X,Y,Z ?

Établissons quelques hypothèses:

  • Appel D.Dispose() pour un IDisposable objet D fondamentalement, les extrémités de sa durée de vie.

  • logiquement, la vie d'un objet ne peut être terminée qu'une seule fois. (Peu importe pour le moment que ceci soit en opposition au protocole IDisposable , qui autorise explicitement les appels multiples à Dispose .)

  • par conséquent, pour des raisons de simplicité, exactement un objet O devrait être responsable de l'élimination D . Appelons O le propriétaire.

VB.NET fournir un mécanisme pour faire respecter les relations de propriété entre les objets. Si cela se transforme en un problème de design: tous les objets O,X,Y,Z qui reçoivent une référence à un autre objet D doivent suivre et adhérer à une convention qui réglemente exactement qui a la propriété sur D .

Simplifiez le problème des agrégats!

le seul meilleur conseil que j'ai trouvé sur ce sujet vient de Eric Evans '2004 book, Domain-Driven Design . Permettez-moi de citer du livre:

dites que vous supprimiez un objet personne d'une base de données. Avec la personne aller un nom, date de naissance, et une description de poste. Mais que penser de l'adresse? Il pourrait y avoir d'autres personnes à la même adresse. Si vous supprimez l'adresse, ces objets de personne auront des références à un objet supprimé. Si vous la laissez, vous accumuler indésirable adresses dans la base de données. La collecte automatique des ordures pourrait éliminer les adresses indésirables, mais cette solution techncal, même si elle est disponible dans votre système de base de données, ignore un problème de base de modélisation. (p. 125)

vous voyez le rapport avec votre problème? Les adresses de cet exemple sont l'équivalent de vos objets jetables, et les questions sont les mêmes: qui devrait les supprimer? Qui les "possède"?

Evans Aggregates " comme solution à ce problème de conception. Du livre encore:

un agrégat est un ensemble d'objets associés que nous traitons comme une unité aux fins des changements de données. Chaque Agrégat a une racine et une frontière. La limite définit ce qui se trouve à l'intérieur de l'agrégat. La racine est une entité unique et spécifique contenue dans L'agrégat. La racine est le seul membre de L'ensemble que les objets extérieurs sont autorisés à contenir des références, bien que les objets à l'intérieur de la limite peuvent contenir des références les uns aux autres. (p. 126 à 127)

le message principal ici est que vous devez restreindre la transmission de votre objet IDisposable à un ensemble strictement limité ("agrégat") d'autres objets. Les objets situés à l'extérieur de cette limite ne devraient jamais faire référence directement à votre IDisposable . Cela simplifie grandement les choses, puisque vous n'avez plus besoin de vous inquiéter si la plus grande partie de tous les objets, à savoir ceux en dehors de l'agrégat, pourrait Dispose votre objet. Tout ce que vous devez faire est de s'assurer que les objets à l'intérieur la frontière tous savent qui est responsable de l'élimination. Cela devrait être un problème assez facile à résoudre, car vous avez généralement les mettre en œuvre ensemble et prendre soin de garder les limites de l'agrégat raisonnablement "serré".

Ce sujet de la suggestion que le créateur d'un IDisposable objet doit également disposer?

cette ligne directrice semble raisonnable et il y a une symétrie attrayante à elle, mais en soi, elle ne fonctionne souvent pas dans la pratique. On peut soutenir que cela signifie la même chose que dire, "Ne jamais passer une référence à un IDisposable objet à un autre objet", parce que dès que vous faites cela, vous risquez que l'objet récepteur suppose sa propriété et il dispose à votre insu.

regardons deux types d'interfaces proéminents de la bibliothèque .NET Base Class Library (BCL) qui violent clairement cette règle empirique: IEnumerable<T> et IObservable<T> . Les deux sont essentiellement des usines qui retournent IDisposable objets:

  • IEnumerator<T> IEnumerable<T>.GetEnumerator()

    (Rappelez-vous que IEnumerator<T> hérite de IDisposable .)

  • IDisposable IObservable<T>.Subscribe(IObserver<T> observer)

dans les deux cas, le appelant est censé disposer de l'objet retourné. On peut soutenir que notre ligne directrice n'a tout simplement pas de sens dans le cas des usines d'objets... à moins, peut-être, que nous exigions que le requérant (et non son "créateur" immédiat 1519560920") du IDisposable le libère.

D'ailleurs, cet exemple aussi démontre les limites de la solution d'agrégat décrite ci-dessus: IEnumerable<T> et IObservable<T> sont de nature trop générale pour jamais faire partie d'un agrégat. Les agrégats sont généralement très spécifiques à un domaine.

autres ressources et idées:

  • in UML, "has a" les relations entre les objets peuvent être modélisées de deux manières: comme agrégation (diamant vide), ou comme composition (rempli diamant.) La Composition diffère de l'agrégation en ce que la durée de vie de l'objet contenu/référé se termine avec celle du conteneur/referrer. Votre question initiale a impliqué l'agrégation ("propriété transférable"), alors que j'ai surtout orienté vers des solutions qui utilisent la composition ("propriété fixe"). Voir le article de Wikipedia sur la "composition D'objet" .

  • Autofac (A. net IoC conteneur) résout ce problème de deux manières: soit en communiquant, en utilisant un soi-disant type de relation , Owned<T> , qui acquiert la propriété d'un IDisposable ; ou par le concept d'unités de travail, appelé portée à vie dans Autofac.

  • en ce qui concerne ce dernier, Nicholas Blum Hardt, le créateur D'Autofac, a écrit " une vie Autofac Primer" , qui comprend une section "IDisposable et de la propriété". The whole article is an excellent treatise on ownership and lifetime issues in .NET. Je recommande de le lire, même pour ceux qui ne sont pas intéressés par Autofac.

  • dans C++, le L'Acquisition de ressources est L'initialisation (RAII) idiom (en général) et types de pointeur intelligent (en particulier) aider le programmeur obtenir l'objet questions de vie et de propriété droit. Malheureusement, ceux-ci ne sont pas transférables à .NET, parce que .NET ne dispose pas du support élégant de C++pour la destruction déterministe d'objets.

  • Voir aussi cette réponse à la question sur Stack Overflow, "Comment disparates besoins de mise en œuvre?" , qui (si je la comprends bien) suit une pensée similaire à ma réponse basée sur L'agrégat: construire un composant à grain grossier autour du IDisposable de sorte qu'il est complètement contenu (et caché du consommateur de Composant) à l'intérieur.

36
répondu stakx 2017-05-23 12:34:35

en général, une fois que vous avez affaire à un objet jetable, vous n'êtes plus dans le monde idéal du code géré où la propriété à vie est un point discutable. En conséquence, vous devez considérer quel objet logiquement "possède", ou est responsable de la vie de votre objet jetable.

généralement, dans le cas d'un objet jetable qui est juste passé dans une méthode, je dirais non, la méthode ne devrait pas disposer l'objet parce qu'il est très rare pour un objet pour assumer la propriété d'un autre objet et être fait avec elle dans la même méthode. L'appelant devrait être responsable de la disposition dans ces cas.

il n'y a pas de réponse automatique qui dit" oui, toujours disposer "ou" non, jamais disposer " quand on parle de données des membres. Plutôt, vous devez penser aux objets dans chaque cas spécifique et vous demander, "cet objet est-il responsable de la vie de l'objet jetable?"

La règle de base est que l'objet responsable de la création d'un jetable le possède, et est donc responsable de le disposer plus tard. Cela ne tient pas si il y a un transfert de propriété. Par exemple:

public class Foo
{
    public MyClass BuildClass()
    {
        var dispObj = new DisposableObj();
        var retVal = new MyClass(dispObj);
        return retVal;
    }
}

Foo est clairement responsable de la création de dispObj , mais il passe la propriété à l'instance de MyClass .

8
répondu Greg D 2010-11-03 10:19:22

C'est une suite à ma réponse précédente ; voir sa remarque initiale pour découvrir pourquoi j'ai écris un autre.

ma réponse précédente a obtenu une chose droite: chaque IDisposable devrait avoir un" propriétaire "exclusif qui sera responsable de Dispose - ing de lui exactement une fois. "1519440920 la" Gestion "de 151930920" objets devient alors très comparable à la gestion de la mémoire dans le code non managé scénarios.

La technologie précédente de .NET, le Component Object Model (COM), utilisait le protocol for memory management responsibilities between objects:

  • "Dans les paramètres doivent être alloués et libéré par l'appelant.
  • "paramètres de Sortie doivent être attribués par l'appelé; ils sont libérés par l'appelant [...].
  • "Dans les paramètres de sortie sont attribuées initialement par l'appelant, et puis libéré et réattribué par celui qui a appelé, si nécessaire. Comme pour les paramètres out, l'appelant est responsable de libérer la valeur finale retournée."

(il existe des règles supplémentaires pour les cas d'erreur; voir la page liée ci-dessus pour plus de détails.)

si nous devions adapter ces lignes directrices pour IDisposable , nous pourrions établir ce qui suit ...

règles concernant IDisposable propriété: (ou transmettre la propriété du IDisposable de la même manière).
  • Lorsqu'un IDisposable est attribué à une méthode par l'intermédiaire d'un paramètre ref , la propriété de cette méthode est transférée à cette méthode. La méthode doit copier le IDisposable dans une variable locale ou un champ d'objet, puis définir le paramètre ref à null .
  • une règle peut-être importante découle de ce qui précède:

    1. Si vous n'avez pas la propriété, vous ne devez pas transmettre. Cela signifie que, si vous avez reçu un objet IDisposable via un paramètre régulier, ne mettez pas le même objet dans un paramètre ref IDisposable , ni ne l'exposez via une valeur de retour ou un paramètre out .

    exemple:

    sealed class LineReader : IDisposable
    {
        public static LineReader Create(Stream stream)
        {
            return new LineReader(stream, ownsStream: false);
        }
    
        public static LineReader Create<TStream>(ref TStream stream) where TStream : Stream
        {
            try     { return new LineReader(stream, ownsStream: true); }
            finally { stream = null;                                   }
        }
    
        private LineReader(Stream stream, bool ownsStream)
        {
            this.stream = stream;
            this.ownsStream = ownsStream;
        }
    
        private Stream stream; // note: must not be exposed via property, because of rule (2)
        private bool ownsStream;
    
        public void Dispose()
        {
            if (ownsStream)
            {
                stream?.Dispose();
            }
        }
    
        public bool TryReadLine(out string line)
        {
            throw new NotImplementedException(); // read one text line from `stream` 
        }
    }
    

    cette classe A deux méthodes statiques d'usine et laisse ainsi son client choisir s'il veut garder ou transmettre la propriété:

    • on accepte un objet Stream via un paramètre régulier. Cela indique à l'appelant que la propriété ne sera pas repris. Ainsi, l'appelant a besoin de Dispose :

      using (var stream = File.OpenRead("Foo.txt"))
      using (var reader = LineReader.Create(stream))
      {
          string line;
          while (reader.TryReadLine(out line))
          {
              Console.WriteLine(line);
          }
      }
      
    • un qui accepte un objet Stream via un paramètre ref . Cela indique à l'appelant que la propriété sera transférée, de sorte que l'appelant n'a pas besoin de Dispose :

      var stream = File.OpenRead("Foo.txt");
      using (var reader = LineReader.Create(ref stream))
      {
          string line;
          while (reader.TryReadLine(out line))
          {
              Console.WriteLine(line);
          }
      }
      

      fait intéressant , si stream était déclaré comme une using variable: using (var stream = …) , la compilation échouerait parce que using variables ne peut pas être passé comme ref paramètres, de sorte que le compilateur C# aide à appliquer nos règles dans ce cas précis.

    enfin, notez que File.OpenRead est un exemple pour une méthode qui retourne un IDisposable objet (à savoir, un Stream ) via la valeur de retour, donc la propriété sur le renvoyé flux est transféré à l'appelant.

    désavantage:

    le principal inconvénient de ce modèle est que AFAIK, personne ne l'utilise (encore). Donc, si vous interagissez avec une API qui ne suit pas les règles ci-dessus (par exemple, la bibliothèque de classe .net Framework base), vous devez tout de même lire la documentation pour savoir qui doit appeler Dispose sur les objets IDisposable .

    7
    répondu stakx 2017-05-23 12:26:15

    une chose que j'ai décidé de faire avant que je savais beaucoup sur la programmation .NET, mais il semble toujours une bonne idée, est d'avoir un constructeur qui accepte un IDisposable accepter également un booléen qui dit si la propriété de l'objet va être transféré aussi. Pour les objets qui peuvent exister entièrement dans le cadre des déclarations using , cela ne sera généralement pas trop important (puisque l'objet extérieur sera disposé dans le cadre de l'utilisation de l'objet interne bloc, il n'y a pas besoin pour l'objet extérieur de disposer de l'intérieur; en effet, il peut être nécessaire de ne pas le faire). Une telle sémantique peut devenir essentielle, cependant, lorsque l'objet extérieur sera passé en tant qu'interface ou classe de base à un code qui ne connaît pas l'existence de l'objet intérieur. Dans ce cas, l'objet interne est censé vivre jusqu'à l'extérieur de l'objet est détruit, et la chose qui sait que l'objet interne est supposé mourir lorsque l'objet extérieur n'est à l'extérieur de l'objet lui-même, de sorte que l'objet extérieur a pour être en mesure de détruire de l'intérieur.

    depuis, j'ai eu quelques idées supplémentaires, mais je ne les ai pas essayées. Je serais curieux de savoir ce que les autres pensent:

    1. une enveloppe de comptage de référence pour un objet IDisposable . Je n'ai pas vraiment compris le modèle le plus naturel pour faire cela, mais si un objet utilise le comptage de référence avec incrément/décrément entrelacés, et si (1) tout le code qui manipule l'objet l'utilise correctement, et (2) aucune référence cyclique n'est créée en utilisant l'objet, je m'attendrais à ce qu'il soit possible d'avoir un objet partagé IDisposable qui est détruit lorsque la dernière utilisation va bye-bye. Probablement ce qui devrait se passer serait que la classe publique soit un wrapper pour une classe de référence privée comptée, et qu'elle supporte une méthode de constructeur ou d'usine qui créera une nouvelle wrapper pour la même instance de base (en boitant le compte de référence de l'instance par un). Ou, si les besoins de la classe de être nettoyé même lorsque les emballages sont abandonnés, et si la classe a une certaine routine de sondage périodique, la classe pourrait garder une liste de WeakReference à Ses Emballages et de vérifier pour s'assurer qu'au moins certains d'entre eux existent toujours.
    2. faire accepter au constructeur un objet IDisposable un délégué qu'il appellera la première fois que l'objet est disposé (un objet IDisposable doit utiliser Interlocked.Exchange sur le drapeau isdisposé pour s'assurer qu'il est disposé exactement une fois). Que le délégué pourrait alors s'occuper de la disposition des objets imbriqués (éventuellement avec un chèque pour voir si quelqu'un d'autre les tenait encore).

    est-ce que l'un ou l'autre semble être un bon modèle?

    2
    répondu supercat 2014-06-03 17:22:11