57 votes

Qui doit appeler Dispose sur les objets IDisposable lorsqu'ils sont passés dans un autre objet?

Y a-t-il des directives ou des bonnes pratiques concernant qui devrait appeler Dispose() sur les objets jetables lorsqu'ils ont été passés aux méthodes ou au constructeur d'un autre objet ?

Voici quelques exemples de ce que je veux dire.

Un objet IDisposable est passé à une méthode (doit-il être jeté une fois terminé?) :

public void FaireQuelqueChose(IDisposable objetJetable)
{
    // Faire quelque chose avec objetJetable
    CalculerQuelqueChose(objetJetable)

    objetJetable.Dispose();
}

Un objet IDisposable est passé à une méthode et une référence est conservée (doit-il être jeté lorsque MaClasse est jetée ?) :

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

    public void FaireQuelqueChose(IDisposable objetJetable)
    {
        _disposableObj = objetJetable;
    }

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

Je pense actuellement que dans le premier exemple, l'appelant de FaireQuelqueChose() devrait jeter l'objet car il l'a probablement créé. Mais dans le deuxième exemple, il semble que MaClasse devrait jeter l'objet car elle conserve une référence. Le problème avec cela est que la classe appelante pourrait ne pas savoir que MaClasse a conservé une référence et pourrait donc décider de jeter l'objet avant que MaClasse ait fini de l'utiliser. Existe-t-il des règles standard pour ce genre de scénario ? Si oui, diffèrent-elles lorsque l'objet jetable est passé dans un constructeur ?

40voto

stakx Points 29832

P.S. : J'ai posté un nouvelle réponse (contenant un ensemble de règles simples pour savoir qui doit appeler Dispose et comment concevoir une interface de programmation (API) qui traite des IDisposable objets). Bien que la présente réponse contienne des idées intéressantes, j'en suis venu à penser que sa principale suggestion ne fonctionne souvent pas dans la pratique : Cacher IDisposable dans des objets à grain plus grossier signifie souvent que ces derniers doivent devenir des objets à grain plus grossier. IDisposable On se retrouve donc au point de départ, et le problème reste entier.


Existe-t-il des conseils ou des bonnes pratiques concernant les personnes à appeler ? Dispose() sur les objets jetables lorsqu'ils ont été passés dans les méthodes ou le constucteur d'un autre objet ?

Réponse courte :

Oui, il existe de nombreux conseils sur ce sujet, et le meilleur que je connaisse est le suivant Eric Evans Le concept de Agrégats en Conception pilotée par domaine . (En d'autres termes, l'idée centrale, telle qu'elle est appliquée aux IDisposable est la suivante : Encapsuler le IDisposable dans un composant à grain plus grossier, de sorte qu'il n'est pas vu de l'extérieur et n'est jamais transmis au consommateur du composant).

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

Le reste de ma réponse aborde plus en détail ces deux points, dans le même ordre. Je terminerai ma réponse par quelques renvois à d'autres documents en rapport avec le même sujet.

Réponse plus longue - Ce dont il est question dans cette question en termes plus généraux :

Les conseils sur ce sujet ne sont généralement pas spécifiques aux IDisposable . Chaque fois que l'on parle de durée de vie et de propriété des objets, on se réfère au même problème (mais en termes plus généraux).

Pourquoi ce sujet ne se pose-t-il pratiquement jamais dans l'écosystème .NET ? Parce que l'environnement d'exécution de .NET (le CLR) effectue un ramassage automatique des ordures, qui fait tout le travail à votre place : Si vous n'avez plus besoin d'un objet, vous pouvez simplement l'oublier et le ramasse-miettes s'en chargera. éventuellement se réapproprier sa mémoire.

Pourquoi, alors, la question se pose-t-elle avec IDisposable objets ? Parce que IDisposable concerne le contrôle explicite et déterministe de la durée de vie d'une ressource (souvent peu abondante ou coûteuse) : IDisposable Les objets sont censés être libérés dès qu'ils ne sont plus nécessaires - et la garantie indéterminée du ramasse-miettes ("Je vais éventuellement se réapproprier le mémoire utilisé par vous !") n'est tout simplement pas suffisant.

Votre question, reformulée en termes plus généraux de durée de vie et de propriété des objets :

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

Posons quelques hypothèses :

  • Appel D.Dispose() pour un IDisposable objet D est en fait la fin de sa durée de vie.

  • Logiquement, la durée de vie d'un objet ne peut être interrompue qu'une seule fois. (Peu importe pour l'instant que cela s'oppose à la règle du IDisposable qui autorise explicitement les appels multiples à Dispose .)

  • Par conséquent, pour des raisons de simplicité, un seul objet O doit être responsable de l'élimination D . Appelons-le O le propriétaire.

Nous arrivons maintenant au cœur du problème : Ni le langage C#, ni VB.NET ne fournissent de mécanisme permettant de renforcer les relations de propriété entre les objets. Il s'agit donc d'un problème de conception : 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égit exactement qui est propriétaire de D .

Simplifiez le problème avec les agrégats !

Le meilleur conseil que j'ai trouvé sur ce sujet est celui de Eric Evans Livre de 2004, Conception pilotée par domaine . Permettez-moi de citer un extrait du livre :

Supposons que vous supprimiez un objet Personne d'une base de données. La personne est accompagnée d'un nom, d'une date de naissance et d'une description de poste. Mais qu'en est-il de l'adresse ? Il peut y avoir d'autres personnes à la même adresse. Si vous supprimez l'adresse, ces objets Personne feront référence à un objet supprimé. Si vous la laissez, vous accumulez des adresses inutiles dans la base de données. Le ramassage automatique des ordures pourrait éliminer les adresses inutiles, mais cette solution technique, même si elle est disponible dans votre système de base de données, ne tient pas compte d'un problème de modélisation fondamental. (p. 125)

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

Evans poursuit en suggérant Agrégats comme solution à ce problème de conception. Encore un extrait du livre :

Un agrégat est un groupe d'objets associés que nous traitons comme une unité pour les modifications de données. Chaque agrégat a une racine et une limite. 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'agrégat auquel les objets extérieurs sont autorisés à faire référence, bien que les objets situés à l'intérieur du périmètre puissent faire référence les uns aux autres. (pp. 126-127)

Le message essentiel est qu'il faut limiter la transmission de vos IDisposable à un ensemble strictement limité ("agrégat") d'autres objets. Les objets situés en dehors de cet ensemble ne doivent jamais obtenir de référence directe à votre objet IDisposable . Cela simplifie grandement les choses, car il n'est plus nécessaire de se demander si la plus grande partie de tous les objets, à savoir ceux qui se trouvent en dehors de l'agrégat, pourrait Dispose votre objet. Il suffit de s'assurer que les objets à l'intérieur la frontière, tous savent qui est responsable de son élimination. Ce problème devrait être assez facile à résoudre, étant donné que vous les mettez généralement en œuvre ensemble et que vous veillez à ce que les limites de l'agrégat soient raisonnablement "étroites".

Qu'en est-il de la suggestion selon laquelle le créateur d'un IDisposable doit également s'en débarrasser ?

Cette ligne directrice semble raisonnable et présente une symétrie attrayante, mais en soi, elle ne fonctionne souvent pas dans la pratique. On peut soutenir qu'elle a la même signification que de dire : "Ne passez jamais une référence à un fichier IDisposable à un autre objet", car dès que vous faites cela, vous risquez que l'objet récepteur suppose sa propriété et en dispose à votre insu.

Examinons deux types d'interface importants de la bibliothèque de classes de base (BCL) .NET qui violent clairement cette règle de base : IEnumerable<T> y IObservable<T> . Les deux sont essentiellement des usines qui renvoient IDisposable objets :

  • IEnumerator<T> IEnumerable<T>.GetEnumerator()
    (Rappelons 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 fabriques d'objets... à moins, peut-être, d'exiger que l'élément demandeur (et non pas son créateur ) de l IDisposable le libère.

Par ailleurs, cet exemple montre également les limites de la solution globale décrite ci-dessus : Les deux IEnumerable<T> y IObservable<T> sont beaucoup trop générales pour faire partie d'un agrégat. Les agrégats sont généralement très spécifiques à un domaine.

Autres ressources et idées :

  • En UML, les relations "a" entre les objets peuvent être modélisées de deux manières : En tant qu'agrégation (losange vide) ou en tant que composition (losange rempli). La composition diffère de l'agrégation en ce sens que la durée de vie de l'objet contenu/référencé prend fin avec celle du conteneur/référent. Votre question initiale impliquait l'agrégation ("propriété transférable"), tandis que je me suis principalement orienté vers des solutions qui utilisent la composition ("propriété fixe"). Voir l'article Article de Wikipédia sur "Composition d'objets" .

  • Autofac (un logiciel .NET IoC ) résout ce problème de deux manières : soit en communiquant, à l'aide d'un "conteneur", ce que l'on appelle le type de relation , Owned<T> qui acquiert la propriété d'un IDisposable ou par le biais du concept d'unités de travail, appelées "lifetime scopes" dans Autofac.

  • À ce sujet, Nicholas Blumhardt, le créateur d'Autofac, a écrit "L'abc de la durée de vie d'Autofac" qui comprend une section intitulée "IDisposable and ownership". L'article entier est un excellent traité sur les questions de propriété et de durée de vie dans .NET. Je recommande sa lecture, même à ceux qui ne sont pas intéressés par Autofac.

  • En C++, la fonction L'acquisition des ressources est l'initialisation (RAII) idiome (en général) et types de pointeurs intelligents (en particulier) aident le programmeur à gérer correctement les questions de durée de vie et de propriété des objets. Malheureusement, elles ne sont pas transférables à .NET, car ce dernier ne dispose pas du support élégant de C++ pour la destruction déterministe des objets.

  • Voir aussi cette réponse à la question sur Stack Overflow, "Comment tenir compte des besoins disparates en matière de mise en œuvre ? qui (si je comprends bien) suit un raisonnement similaire à celui de ma réponse basée sur l'agrégat : Construire un composant à gros grain autour de l'agrégat IDisposable de manière à ce qu'il soit complètement contenu (et caché au consommateur du composant) à l'intérieur.

35voto

Mark Byers Points 318575

Une règle générale est que si vous avez créé (ou acquis la propriété) de l'objet, il est de votre responsabilité de le disposer. Cela signifie que si vous recevez un objet jetable en tant que paramètre dans une méthode ou un constructeur, vous ne devriez généralement pas le disposer.

Notez que certaines classes du framework .NET disposent des objets qu'elles ont reçus en tant que paramètres. Par exemple, disposer d'un StreamReader dispo.de également le Stream sous-jacent.

16voto

stakx Points 29832

Ceci est un suivi de ma réponse précédente; voir son remarque initiale pour comprendre pourquoi je poste une autre réponse.

Ma réponse précédente avait raison sur un point: Chaque IDisposable devrait avoir un "propriétaire" exclusif qui sera responsable de l'appel à Dispose exactement une fois. La gestion d'objets IDisposable devient alors très similaire à celle de la gestion de mémoire dans des scénarios de code non managé.

La technologie précurseur de .NET, le Component Object Model (COM), utilisait le protocole de gestion de mémoire suivant entre les objets:

  • "Les paramètres d'entrée doivent être alloués et libérés par l'appelant.
  • "Les paramètres de sortie doivent être alloués par l'appelé; ils sont libérés par l'appelant [...].
  • "Les paramètres d'entrée-sortie sont initialement alloués par l'appelant, puis libérés et réalloués par l'appelé, si nécessaire. Comme c'est le cas pour les paramètres de sortie, l'appelant est responsable de libérer la valeur finale retournée."

(Il y a des règles supplémentaires pour les cas d'erreur; consultez la page liée ci-dessus pour plus de détails.)

Si nous devions adapter ces directives pour les IDisposable, nous pourrions établir les règles suivantes…

Règles concernant la possession d'un IDisposable:

  1. Lorsqu'un IDisposable est passé à une méthode via un paramètre standard, il n'y a pas de transfert de possession. La méthode appelée peut utiliser le IDisposable, mais ne doit pas l'appeler à Dispose (ni transférer la possession; voir règle 4 ci-dessous).
  2. Lorsqu'un IDisposable est retourné à partir d'une méthode via un paramètre out ou la valeur de retour, alors la possession est transférée de la méthode à son appelant. L'appelant devra l'appeler à Dispose (ou transférer la possession sur le IDisposable de la même manière).
  3. Lorsqu'un IDisposable est donné à une méthode via un paramètre ref, alors la possession est transférée à cette méthode. La méthode devrait copier le IDisposable dans une variable locale ou un champ d'objet, puis définir le paramètre ref à null.

Une règle potentiellement importante découle des éléments ci-dessus:

  1. Si vous n'avez pas la possession, vous ne devez pas la transférer. Cela signifie que si vous avez reçu un objet IDisposable via un paramètre standard, ne placez 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(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 de création statiques et permet ainsi à son client de choisir s'il souhaite conserver ou transférer la possession:

  • Une qui accepte un objet Stream via un paramètre standard. Cela signale à l'appelant que la possession ne sera pas transférée. Ainsi, l'appelant doit appeler à Dispose:

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

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

    Intéressant, si stream était déclaré comme une variable using: using (var stream = …), la compilation échouerait car les variables using ne peuvent pas être passées comme paramètres ref, donc le compilateur C# aide à faire respecter nos règles dans ce cas spécifique.

Enfin, notons que File.OpenRead est un exemple d'une méthode qui renvoie un objet IDisposable (à savoir, un Stream) via la valeur de retour, donc la possession sur le flux retourné est transférée à l'appelant.

Inconvénient:

Le principal inconvénient de ce schéma est qu'à ma connaissance, personne ne l'utilise (pour le moment). Donc si vous interagissez avec une API qui ne suit pas les règles ci-dessus (par exemple, la Bibliothèque de classes de base de .NET Framework), vous devrez toujours lire la documentation pour savoir qui doit appeler Dispose sur les objets IDisposable.

8voto

Greg D Points 24218

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

En général, dans le cas d'un objet jetable passé simplement dans une méthode, je dirais que non, la méthode ne devrait pas disposer de l'objet car il est très rare qu'un objet assume la propriété d'un autre objet et s'en débarrasse ensuite dans la même méthode. L'appelant devrait être responsable de la mise au rebut dans ces cas-là.

Il n'y a pas de réponse automatique qui dise "Oui, toujours disposer" ou "Non, ne jamais disposer" en ce qui concerne les données membres. Au contraire, vous devez réfléchir aux objets dans chaque cas spécifique et vous demander : "Cet objet est-il responsable de la durée de vie de l'objet jetable ?"

La règle de base est que l'objet responsable de la création d'un objet jetable le possède, et est donc responsable de le disposer plus tard. Cela ne s'applique pas en cas de 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 transfère la propriété à l'instance de MyClass.

2voto

supercat Points 25534

Une chose que j'ai décidé de faire avant d'en savoir beaucoup sur la programmation .NET, mais qui semble toujours être une bonne idée, est d'avoir un constructeur qui accepte une IDisposable d'accepter également un booléen indiquant si la propriété de l'objet va être transférée également. 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 externe sera disposé dans le cadre du bloc Using de l'objet interne, il n'est pas nécessaire que l'objet externe dispose de l'objet interne ; en effet, il peut être nécessaire de ne pas le faire). Ces sémantiques peuvent devenir essentielles, cependant, lorsque l'objet externe sera passé en tant qu'interface ou classe de base à du code qui ne connaît pas l'existence de l'objet interne. Dans ce cas, l'objet interne est censé vivre jusqu'à ce que l'objet externe soit détruit, et la seule chose qui connaît l'existence de l'objet interne et qui doit mourir lorsque l'objet externe le fait est l'objet externe lui-même, de sorte que l'objet externe doit être capable de détruire l'objet interne.

Depuis lors, 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. Un wrapper de comptage de références pour un objet IDisposable. Je n'ai pas vraiment trouvé le modèle le plus naturel pour le faire, mais si un objet utilise le comptage de références avec increment/décrement interlocked, et si (1) tout le code qui manipule l'objet l'utilise correctement, et si (2) aucune référence cyclique n'est créée en utilisant l'objet, je m'attends à ce qu'il soit possible d'avoir un objet IDisposable partagé qui sera détruit lorsque la dernière utilisation disparaîtra. Probablement ce qui devrait se passer serait que la classe publique devrait être un wrapper pour une classe privée avec comptage de références, et elle devrait prendre en charge un constructeur ou une méthode de fabrication qui créera un nouveau wrapper pour la même instance de base (en augmentant le compte de références de l'instance de un). Ou, si la classe doit être nettoyée même lorsque les wrappers sont abandonnés, et si la classe a une routine de sondage périodique, la classe pourrait conserver une liste de WeakReference à ses wrappers et vérifier pour s'assurer qu'au moins certains d'entre eux existent toujours.
  2. Avoir le constructeur d'un objet IDisposable accepter un délégué qu'il appellera la première fois que l'objet sera disposé (un objet IDisposable devrait utiliser Interlocked.Exchange sur le drapeau isDisposed pour s'assurer qu'il est disposé exactement une fois). Ce délégué pourrait alors se charger de disposer de tout objet imbriqué (éventuellement en vérifiant si quelqu'un d'autre les tenait toujours).

L'un de ceux-ci semble-t-il être un bon modèle?

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