Comment empêcher IDisposable de se propager à toutes vos classes?

Commencez avec ces classes simples …

Disons que j’ai un ensemble simple de classes comme ceci:

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; } 

Un Bus a un Driver , le Driver a deux Shoe , chaque Shoe a un Shoelace . Tous très stupides.

Ajouter un object IDisposable à Shoelace

Plus tard, je décide que certaines opérations sur le Shoelace pourraient être multi-threadées, donc j’ajoute un EventWaitHandle pour les threads avec EventWaitHandle communiquer. Donc, Shoelace ressemble maintenant à ceci:

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

Implémenter IDisposable sur le lacet

Mais maintenant, Microsoft FxCop va se plaindre: “Implémenter IDisposable sur ‘Shoelace’ car il crée des membres des types suivants IDisposable: ‘EventWaitHandle’.”

Ok, IDisposable sur Shoelace et ma petite classe soignée devient ce désordre horrible:

 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 le soulignent les commentateurs), puisque Shoelace elle-même ne possède pas de ressources non managées, je pourrais utiliser l’implémentation de disposition la plus simple sans avoir besoin de Dispose(bool) et de 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); } } 

Regarder avec horreur les spreads identifiables

C’est ça qui est réglé. Mais maintenant, FxCop va se plaindre que Shoe crée un Shoelace , donc Shoe doit être IDisposable aussi.

Et le Driver crée une Shoe pour que le Driver soit IDisposable . Et Bus crée un Driver pour que le Bus soit IDisposable , etc.

Tout à coup, mon petit changement à Shoelace me cause beaucoup de travail et mon patron se demande pourquoi je dois payer pour changer de Shoelace .

La question

Comment empêchez-vous cette propagation d’ IDisposable , mais assurez-vous toujours que vos objects non gérés sont correctement éliminés?

Vous ne pouvez pas vraiment “empêcher” de se diffuser. Certaines classes doivent être éliminées, comme AutoResetEvent , et le moyen le plus efficace est de le faire dans la méthode Dispose() 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 être jetables, etc. La seule façon de l’éviter est de:

  • évitez d’utiliser des classes identifiables dans la mesure du 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 modèle à using )

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 un nom n’est pas 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 reflection approfondie sur l’utilisation ou non d’une ressource non gérée peut être pédagogique. Peut donc examiner les sources disponibles pour les bibliothèques .net ou utiliser un décompilateur.

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

Ce que vous pouvez faire est d’éviter d’append un IDisposable à une classe de feuille dans votre hiérarchie d’object. Ce n’est pas toujours une tâche facile, mais c’est un exercice intéressant. D’un sharepoint vue logique, il n’y a aucune raison qu’un ShoeLace doive être jetable. Au lieu d’append un WaitHandle ici, il est également possible d’append une association entre un ShoeLace et un WaitHandle au moment où il est utilisé. Le moyen le plus simple est d’utiliser une instance de dictionnaire.

Si vous pouvez déplacer le WaitHandle en une association libre via une carte au moment où le WaitHandle est réellement utilisé, vous pouvez casser cette chaîne.

Pour empêcher la propagation de IDisposable , vous devez essayer d’encapsuler l’utilisation d’un object jetable dans une seule méthode. Essayez de concevoir Shoelace diffé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 scope du handle d’attente est limitée à la méthode Tie , et la classe n’a pas besoin d’avoir un champ jetable, elle ne doit donc pas nécessairement être elle-même disponible.

Comme le handle d’attente est un détail d’implémentation à l’intérieur du Shoelace , il ne doit en aucun cas modifier son interface publique, comme l’ajout d’une nouvelle interface dans sa déclaration. Que se passera-t-il alors lorsque vous n’avez plus besoin d’un champ jetable, supprimerez-vous la déclaration IDisposable ? Si vous pensez à l’ abstraction de Shoelace , vous réalisez rapidement qu’il ne devrait pas être pollué par des dépendances d’infrastructure, comme IDisposable . IDisposable devrait ê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 .

Cela ressemble beaucoup à un problème de conception de haut niveau, comme c’est souvent le cas lorsqu’un «correctif rapide» se transforme en un bourbier. Pour plus d’informations sur les moyens de sortir, vous trouverez peut-être ce fil utile.

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

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

Ensuite, lorsque la Shoe est rendue IDisposable , FxCop (v1.36) ne se plaint pas du fait que Driver doit également être IDisposable .

Cependant si elle est définie comme ceci:

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

alors il va se plaindre.

Je soupçonne qu’il ne s’agit que d’une limitation de FxCop, plutôt que d’une solution, car dans la première version, les instances de Shoe sont toujours créées par le Driver et doivent toujours être éliminées.

Je ne pense pas qu’il existe un moyen technique d’empêcher l’IDisposable de se propager si vous conservez votre conception si étroitement liée. On devrait 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 général, les chauffeurs de bus ne suivent pas les bus jusqu’à la casse 🙂 Dans le cas des conducteurs et des chaussures, les conducteurs 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 à résoudre. La bonne chose est que les bus ne doivent plus être jetables simplement pour se débarrasser des lacets.

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 refaire le waitHandle en lacets.

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 le vide virtuel Dispose (bool disposant) un peu.

Que diriez-vous d’utiliser Inversion of Control?

 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(ssortingng[] 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))})); } } }