Comment empêchez-vous IDisposable de se propager dans toutes vos classes?

138

Commencez par ces classes simples ...

Disons que j'ai un simple ensemble de classes comme celui-ci:

class Bus
{
    Driver busDriver = new Driver();
}

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

class Shoe
{
    Shoelace lace = new Shoelace();
}

class Shoelace
{
    bool tied = false;
}

A Busa un Driver, le Drivera deux Shoes, chacun Shoea un Shoelace. Tout cela est très idiot.

Ajouter un objet IDisposable à Shoelace

Plus tard, je décide qu'une opération sur le Shoelacepourrait être multi-thread, donc j'ajoute un EventWaitHandlepour les threads avec lesquels communiquer. Alors Shoelacemaintenant ressemble à ceci:

class Shoelace
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    // ... other stuff ..
}

Implémenter IDisposable sur Shoelace

Mais maintenant FxCop de Microsoft se plaindra: "Implémentez IDisposable sur 'Shoelace' car il crée des membres des types IDisposable suivants: 'EventWaitHandle'."

D' accord, je mets en œuvre IDisposablesur Shoelaceet ma petite classe propre cette horrible gâchis devient:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    private bool disposed = false;

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

    ~Shoelace()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                if (waitHandle != null)
                {
                    waitHandle.Close();
                    waitHandle = null;
                }
            }
            // No unmanaged resources to release otherwise they'd go here.
        }
        disposed = true;
    }
}

Ou (comme l'ont souligné les commentateurs) étant donné que Shoelacelui-même n'a pas de ressources non gérées, je pourrais utiliser l'implémentation plus simple de dispose sans avoir besoin de Dispose(bool)et Destructor:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;

    public void Dispose()
    {
        if (waitHandle != null)
        {
            waitHandle.Close();
            waitHandle = null;
        }
        GC.SuppressFinalize(this);
    }
}

Regardez avec horreur alors que IDisposable se propage

C'est bien ça fixe. Mais maintenant FxCop se plaindra que Shoecrée un Shoelace, donc Shoedoit l'être IDisposableaussi.

Et Drivercrée Shoeainsi Driverdoit être IDisposable. Et Buscrée Driverainsi Busdoit être IDisposableet ainsi de suite.

Soudain, mon petit changement Shoelaceme cause beaucoup de travail et mon patron se demande pourquoi je dois payer Buspour faire un changement Shoelace.

La question

Comment éviter cette propagation IDisposable, tout en vous assurant que vos objets non gérés sont correctement éliminés?

GrahamS
la source
9
Une question exceptionnellement bonne, je pense que la réponse est de minimiser leur utilisation et d'essayer de garder les IDisposables de haut niveau de courte durée avec l'utilisation, mais ce n'est pas toujours possible (en particulier lorsque ces IDisposables sont dus à une interopérabilité avec des dll C ++ ou similaires). Regardez attentivement les réponses.
annakata
Ok Dan, j'ai mis à jour la question pour montrer les deux méthodes d'implémentation d'IDisposable sur Shoelace.
GrahamS
Je me méfie généralement des détails d'implémentation d'autres classes pour me protéger. Inutile de le risquer si je peux facilement l'empêcher. Peut-être que je suis trop prudent ou peut-être ai-je simplement passé trop de temps en tant que programmeur C, mais je préfère adopter l'approche irlandaise: "Pour être sûr, pour être sûr" :)
GrahamS
@Dan: La vérification de null est toujours nécessaire pour s'assurer que l'objet lui-même n'a pas été défini sur null, auquel cas l'appel à waitHandle.Dispose () lèvera une NullReferenceException.
Scott Dorman
1
Quoi qu'il en soit, vous devriez toujours utiliser la méthode Dispose (bool) comme indiqué dans votre section «Implémenter IDisposable sur Shoelace» puisque cela (moins le finaliseur) est le modèle complet. Ce n'est pas parce qu'une classe implémente IDisposable qu'elle a besoin d'un finaliseur.
Scott Dorman

Réponses:

36

Vous ne pouvez pas vraiment «empêcher» IDisposable de se propager. Certaines classes doivent être supprimées, par exemple AutoResetEvent, et le moyen le plus efficace est de le faire dans la Dispose()méthode pour éviter la surcharge des finaliseurs. Mais cette méthode doit être appelée d'une manière ou d'une autre, donc exactement comme dans votre exemple, les classes qui encapsulent ou contiennent IDisposable doivent les éliminer, elles doivent donc également être jetables, etc. La seule façon de l'éviter est de:

  • évitez d'utiliser les classes IDisposable lorsque cela est possible, verrouillez ou attendez des événements dans des endroits uniques, conservez des ressources coûteuses en un seul endroit, etc.
  • créez-les uniquement lorsque vous en avez besoin et jetez-les juste après (le usingmotif)

Dans certains cas, IDisposable peut être ignoré car il prend en charge un cas facultatif. Par exemple, WaitHandle implémente IDisposable pour prendre en charge un Mutex nommé. Si aucun nom n'est utilisé, la méthode Dispose ne fait rien. MemoryStream est un autre exemple, il n'utilise aucune ressource système et son implémentation Dispose ne fait rien non plus. Une réflexion approfondie sur l'utilisation ou non d'une ressource non gérée peut être une instruction. Ainsi peut examiner les sources disponibles pour les bibliothèques .net ou utiliser un décompilateur.

Grzenio
la source
4
Le conseil que vous pouvez l'ignorer parce que l'implémentation d'aujourd'hui ne fait rien est mauvais, car une future version pourrait en fait faire quelque chose d'important dans Dispose, et maintenant vous avez du mal à trouver une fuite.
Andy
1
"garder des ressources chères", les IDisposables ne sont pas forcément chers, en effet cet exemple qui utilise une poignée d'attente régulière est à peu près un "poids léger" comme vous le faites.
markmnl
20

En termes d'exactitude, vous ne pouvez pas empêcher la propagation de IDisposable via une relation d'objet si un objet parent crée et possède essentiellement un objet enfant qui doit maintenant être jetable. FxCop est correct dans cette situation et le parent doit être IDisposable.

Ce que vous pouvez faire, c'est éviter d'ajouter un IDisposable à une classe feuille dans votre hiérarchie d'objets. Ce n'est pas toujours une tâche facile mais c'est un exercice intéressant. D'un point de vue logique, il n'y a aucune raison pour laquelle un ShoeLace doit être jetable. Au lieu d'ajouter un WaitHandle ici, il est également possible d'ajouter une association entre un ShoeLace et un WaitHandle au point où il est utilisé. Le moyen le plus simple consiste à utiliser une instance de Dictionary.

Si vous pouvez déplacer le WaitHandle dans une association lâche via une carte au point où le WaitHandle est réellement utilisé, vous pouvez rompre cette chaîne.

JaredPar
la source
2
C'est très étrange de faire une association là-bas. Cet AutoResetEvent est privé pour l'implémentation Shoelace, donc l'exposer publiquement serait erroné à mon avis.
GrahamS
@GrahamS, je ne dis pas de l'exposer publiquement. Je dis peut-il être déplacé au point où les lacets sont attachés. Peut-être que si nouer des lacets de chaussures est une tâche si complexe, ils devraient être une classe de niveau de lacets.
JaredPar
1
Pourriez-vous élargir votre réponse avec quelques extraits de code JaredPar? J'étire peut-être un peu mon exemple mais j'imagine que Shoelace crée et démarre le fil Tyer qui attend patiemment à waitHandle.WaitOne () Le Shoelace appellerait waitHandle.Set () quand il voulait que le fil commence à se lier.
GrahamS
1
+1 à ce sujet. Je pense qu'il serait étrange que seulement Shoelace soit appelé simultanément mais pas les autres. Pensez à ce que devrait être la racine agrégée. Dans ce cas, il devrait être Bus, IMHO, bien que je ne connaisse pas le domaine. Donc, dans ce cas, le bus doit contenir le waithandle et toutes les opérations sur le bus et tous ses enfants seront synchronisées.
Johannes Gustafsson le
16

Pour éviter IDisposablede se propager, vous devriez essayer d'encapsuler l'utilisation d'un objet jetable à l'intérieur d'une seule méthode. Essayez de concevoir Shoelacedifféremment:

class Shoelace { 
  bool tied = false; 

  public void Tie() {

    using (var waitHandle = new AutoResetEvent(false)) {

      // you can even pass the disposable to other methods
      OtherMethod(waitHandle);

      // or hold it in a field (but FxCop will complain that your class is not disposable),
      // as long as you take control of its lifecycle
      _waitHandle = waitHandle;
      OtherMethodThatUsesTheWaitHandleFromTheField();

    } 

  }
} 

La portée du handle d'attente est limitée à la Tieméthode, et la classe n'a pas besoin d'avoir un champ jetable, et n'a donc pas besoin d'être elle-même jetable.

Puisque le handle d'attente est un détail d'implémentation à l'intérieur de Shoelace, il ne devrait en aucun cas changer son interface publique, comme l'ajout d'une nouvelle interface dans sa déclaration. Que se passera-t-il alors si vous n'avez plus besoin d'un champ jetable, supprimerez-vous la IDisposabledéclaration? Si vous pensez à l' Shoelace abstraction , vous vous rendez vite compte qu'elle ne devrait pas être polluée par des dépendances d'infrastructure, comme IDisposable. IDisposabledevrait être réservé aux classes dont l'abstraction encapsule une ressource qui appelle un nettoyage déterministe; c'est-à-dire pour les classes où la disponibilité fait partie de l' abstraction .

Jordão
la source
1
Je suis tout à fait d'accord pour dire que le détail d'implémentation de Shoelace ne doit pas polluer l'interface publique, mais mon point est que cela peut être difficile à éviter. Ce que vous suggérez ici ne serait pas toujours possible: le but de AutoResetEvent () est de communiquer entre les threads, donc sa portée s'étendrait généralement au-delà d'une seule méthode.
GrahamS
@GrahamS: c'est pourquoi j'ai dit d'essayer de le concevoir de cette façon. Vous pouvez également passer le jetable à d'autres méthodes. Il ne tombe en panne que lorsque des appels externes à la classe contrôlent le cycle de vie du jetable. Je mettrai à jour la réponse en conséquence.
Jordão
2
désolé, je sais que vous pouvez faire circuler le jetable, mais je ne vois toujours pas que cela fonctionne. Dans mon exemple, le AutoResetEventest utilisé pour communiquer entre différents threads qui opèrent dans la même classe, il doit donc s'agir d'une variable membre. Vous ne pouvez pas limiter sa portée à une méthode. (par exemple, imaginez qu'un thread attend juste du travail en bloquant waitHandle.WaitOne(). Le thread principal appelle alors la shoelace.Tie()méthode, qui fait juste un waitHandle.Set()et retourne immédiatement).
GrahamS
3

C'est essentiellement ce qui se passe lorsque vous mélangez la composition ou l'agrégation avec des classes jetables. Comme mentionné, la première solution serait de refactoriser le waitHandle hors du lacet.

Cela dit, vous pouvez réduire considérablement le modèle jetable lorsque vous ne disposez pas de ressources non gérées. (Je cherche toujours une référence officielle pour cela.)

Mais vous pouvez omettre le destructeur et GC.SuppressFinalize (this); et peut-être nettoyer un peu le vide virtuel Dispose (suppression des booléens).

Henk Holterman
la source
Merci Henk. Si je retirais la création de waitHandle de Shoelace, alors quelqu'un doit encore s'en débarrasser quelque part alors comment pourrait- il savoir que Shoelace en a fini avec (et que Shoelace ne l'a pas passé à d'autres classes)?
GrahamS
Vous aurez besoin de déplacer non seulement la création mais aussi la «responsabilité» du serveur. La réponse de jaredPar a un début d'idée intéressant.
Henk Holterman
Ce blog couvre l'implémentation d'IDisposable, et explique en particulier comment simplifier la tâche en évitant de mélanger les ressources gérées et non gérées dans une seule classe blog.stephencleary.com/2009/08/…
PaulS
3

Fait intéressant si Driverest défini comme ci-dessus:

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

Ensuite, quand Shoec'est fait IDisposable, FxCop (v1.36) ne se plaint pas qui Driverdevrait aussi l'être IDisposable.

Cependant, s'il est défini comme ceci:

class Driver
{
    Shoe leftShoe = new Shoe();
    Shoe rightShoe = new Shoe();
}

alors il se plaindra.

Je soupçonne que ce n'est qu'une limitation de FxCop, plutôt qu'une solution, car dans la première version, les Shoeinstances sont toujours créées par le Driveret doivent toujours être supprimées.

GrahamS
la source
2
Si Show implémente IDisposable, le créer dans un initialiseur de champ est dangereux. Il est intéressant que FXCop ne permette pas de telles initialisations, car en C #, la seule façon de les faire en toute sécurité est d'utiliser des variables ThreadStatic (carrément hideuses). Dans vb.net, il est possible d'initialiser des objets IDisposable en toute sécurité sans variables ThreadStatic. Le code est toujours moche, mais pas aussi affreux. Je souhaite que MS fournisse un bon moyen d'utiliser de tels initialiseurs de champ en toute sécurité.
supercat
1
@supercat: merci. Pouvez-vous expliquer pourquoi c'est dangereux? Je suppose que si une exception était lancée dans l'un des Shoeconstructeurs, shoesserait-elle laissée dans un état difficile?
GrahamS
3
À moins que l'on sache qu'un type particulier d'IDisposable peut être abandonné en toute sécurité, il faut s'assurer que chaque objet créé est éliminé. Si un IDisposable est créé dans l'initialiseur de champ d'un objet mais qu'une exception est levée avant que l'objet ne soit complètement construit, l'objet et tous ses champs seront abandonnés. Même si la construction de l'objet est enveloppée dans une méthode de fabrique qui utilise un bloc "try" pour détecter que la construction a échoué, il est difficile pour la fabrique d'obtenir des références aux objets à éliminer.
supercat
3
Le seul moyen que je connaisse pour gérer cela en C # serait d'utiliser une variable ThreadStatic pour conserver une liste d'objets qui devront être éliminés si le constructeur actuel lève. Ensuite, demandez à chaque initialiseur de champ d'enregistrer chaque objet au fur et à mesure de sa création, demandez à la fabrique d'effacer la liste si elle se termine avec succès et d'utiliser une clause "finally" pour supprimer tous les éléments de la liste si elle n'a pas été effacée. Ce serait une approche viable, mais les variables ThreadStatic sont laides. Dans vb.net, on pourrait utiliser une technique similaire sans avoir besoin de variables ThreadStatic.
supercat
3

Je ne pense pas qu'il existe un moyen technique d'empêcher IDisposable de se propager si vous gardez votre conception si étroitement couplée. Il faut alors se demander si le design est correct.

Dans votre exemple, je pense qu'il est logique que la chaussure possède le lacet, et peut-être que le conducteur devrait posséder ses chaussures. Cependant, le bus ne devrait pas posséder le conducteur. En règle générale, les chauffeurs de bus ne suivent pas les bus jusqu'à la casse :) Dans le cas des chauffeurs et des chaussures, les chauffeurs fabriquent rarement leurs propres chaussures, ce qui signifie qu'ils ne les «possèdent» pas vraiment.

Une conception alternative pourrait être:

class Bus
{
   IDriver busDriver = null;
   public void SetDriver(IDriver d) { busDriver = d; }
}

class Driver : IDriver
{
   IShoePair shoes = null;
   public void PutShoesOn(IShoePair p) { shoes = p; }
}

class ShoePairWithDisposableLaces : IShoePair, IDisposable
{
   Shoelace lace = new Shoelace();
}

class Shoelace : IDisposable
{
   ...
}

La nouvelle conception est malheureusement plus compliquée, car elle nécessite des classes supplémentaires pour instancier et éliminer des instances concrètes de chaussures et de pilotes, mais cette complication est inhérente au problème en cours de résolution. La bonne chose est que les bus ne doivent plus être jetables simplement pour se débarrasser des lacets.

Joh
la source
2
Cela ne résout cependant pas vraiment le problème d'origine. Cela pourrait garder FxCop silencieux, mais quelque chose devrait tout de même éliminer le lacet.
AndrewS le
Je pense que vous n'avez fait que déplacer le problème ailleurs. ShoePairWithDisposableLaces possède désormais Shoelace (), il doit donc également être rendu IDisposable - alors qui se débarrasse des chaussures? Laissez-vous cela à un conteneur IoC à gérer?
GrahamS
@AndrewS: En effet, quelque chose doit encore disposer des conducteurs, des chaussures et des lacets, mais l'élimination ne doit pas être initiée par les bus. @GrahamS: Vous avez raison, je déplace le problème ailleurs, car je pense qu'il appartient ailleurs. En règle générale, il y aurait une classe d'instanciation des chaussures, qui serait également responsable de l'élimination des chaussures. J'ai modifié le code pour rendre ShoePairWithDisposableLaces jetable également.
Joh
@Joh: Merci. Comme vous le dites, le problème avec son déplacement ailleurs est que vous créez beaucoup plus de complexité. Si ShoePairWithDisposableLaces est créé par une autre classe, alors cette classe ne devrait-elle pas être notifiée lorsque le pilote a fini avec ses chaussures, afin de pouvoir en disposer correctement ()?
GrahamS
1
@Graham: oui, un mécanisme de notification serait nécessaire. Une politique d'élimination basée sur des comptages de référence pourrait être utilisée, par exemple. Il y a une certaine complexité supplémentaire, mais comme vous le savez probablement, "Il n'y a pas de chaussure gratuite" :)
Joh
1

Que diriez-vous d'utiliser l'inversion de contrôle?

class Bus
{
    private Driver busDriver;

    public Bus(Driver busDriver)
    {
        this.busDriver = busDriver;
    }
}

class Driver
{
    private Shoe[] shoes;

    public Driver(Shoe[] shoes)
    {
        this.shoes = shoes;
    }
}

class Shoe
{
    private Shoelace lace;

    public Shoe(Shoelace lace)
    {
        this.lace = lace;
    }
}

class Shoelace
{
    bool tied;
    private AutoResetEvent waitHandle;

    public Shoelace(bool tied, AutoResetEvent waitHandle)
    {
        this.tied = tied;
        this.waitHandle = waitHandle;
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var leftShoeWaitHandle = new AutoResetEvent(false))
        using (var rightShoeWaitHandle = new AutoResetEvent(false))
        {
            var bus = new Bus(new Driver(new[] {new Shoe(new Shoelace(false, leftShoeWaitHandle)),new Shoe(new Shoelace(false, rightShoeWaitHandle))}));
        }
    }
}
Darragh
la source
Maintenant, vous en avez fait le problème des appelants, et Shoelace ne peut pas dépendre de la poignée d'attente disponible quand il en a besoin.
Andy
@andy Que voulez-vous dire par "Shoelace ne peut pas dépendre de la poignée d'attente disponible quand il en a besoin"? Il est transmis au constructeur.
Darragh
Quelque chose d'autre aurait pu perturber l'état des événements automatiques avant de le donner à Shoelace, et cela pourrait commencer avec l'ARE dans un mauvais état; quelque chose d'autre pourrait se brouiller avec l'état de l'ARE pendant que Shoelace fait son travail, poussant Shoelace à faire la mauvaise chose. C'est la même raison pour laquelle vous ne verrouillez que les membres privés. "En général, .. évitez de verrouiller des instances indépendantes de votre contrôle par les codes" docs.microsoft.com/en-us/dotnet/csharp/language-reference
Andy
Je suis d'accord pour ne verrouiller que les membres privés. Mais il semble que les poignées d'attente soient différentes. En fait, il semble plus utile de les transmettre à l'objet car la même instance doit être utilisée pour communiquer entre les threads. Néanmoins, je pense qu'IoC fournit une solution utile au problème du PO.
Darragh
Étant donné que l'exemple OPs avait le waithandle en tant que membre privé, l'hypothèse sûre est que l'objet attend un accès exclusif à celui-ci. Rendre la poignée visible à l'extérieur de l'instance enfreint cela. En règle générale, un IoC peut être bon pour des choses comme celle-ci, mais pas pour le threading.
Andy