Implémentation minimale identifiable pour les ressources gérées uniquement

Il y a BEAUCOUP d’informations sur l’implémentation IDisposable “standard complète” pour l’élimination des ressources non gérées – mais en réalité, ce cas est (très) rare (la plupart des ressources sont déjà encapsulées par des classes gérées). Cette question porte sur une implémentation minimale d’IDisposable dans le cas beaucoup plus courant de “ressources gérées uniquement”.

1: L’implémentation minimale d’ IDisposable dans le code ci-dessous est-elle correcte, existe-t-il des problèmes?

2: Y a-t-il une raison d’append une implémentation IDisposable standard complète ( Dispose() , Dispose(bool) , Finalizer etc.) sur l’implémentation minimale présentée?

3: Est-il correct / sage dans ce cas minimal de rendre le Dispose virtuel (puisque nous ne fournissons pas Dispose(bool) )?

4: Si cette implémentation minimale est remplacée par une implémentation standard complète incluant un finaliseur (inutile, dans ce cas), cela change-t-il la manière dont le GC gère l’object? Y a-t-il des inconvénients?

5: L’exemple inclut les gestionnaires d’événements et de Timer , car il est particulièrement important de ne pas rater ces cas. Ne pas les mettre au rebut gardera les objects en vie ( this dans le cas de Timer , eventSource dans le cas du gestionnaire d’événements) jusqu’à ce que le GC se eventSource de les disposer en son temps. Y at-il d’autres exemples comme ceux-ci?

 class A : IDisposable { private Timer timer; public A(MyEventSource eventSource) { eventSource += Handler } private void Handler(object source, EventArgs args) { ... } public virtual void Dispose() { timer.Dispose(); if (eventSource != null) eventSource -= Handler; } } class B : A, IDisposable { private TcpClient tpcClient; public override void Dispose() { (tcpClient as IDispose).Dispose(); base.Dispose(); } } 

réfs:
MSDN
SO: Quand dois-je gérer les ressources gérées
SO: Comment supprimer une ressource gérée dans la méthode Dispose () en C #
SO: Dispose () pour le nettoyage des ressources gérées

  1. L’implémentation est correcte, il n’y a pas de problèmes, à condition qu’aucune classe dérivée ne possède directement une ressource non gérée.

  2. Le “principe de la moindre surprise” est une bonne raison de mettre en œuvre le schéma complet. Comme il n’y a pas de document faisant autorité dans MSDN décrivant ce modèle plus simple, les développeurs de maintenance à venir pourraient avoir des doutes – même si vous avez ressenti le besoin de demander à StackOverflow 🙂

  3. Oui, il est normal que Dispose soit virtuel dans ce cas.

  4. Le temps système du finisseur inutile est négligeable si Dispose a été appelé et est correctement implémenté (c.-à-d., Appelle GC.SuppressFinalize).

La très grande majorité des classes IDisposable dehors du .NET Framework lui-même sont IDisposable car elles possèdent des ressources gérées IDisposable . Il est rare qu’ils détiennent directement une ressource non gérée – cela ne se produit que lorsque P / Invoke est utilisé pour accéder à des ressources non gérées qui ne sont pas exposées par le .NET Framework.

Par conséquent, il existe probablement un bon argument pour promouvoir ce modèle plus simple:

  • Dans les rares cas où des ressources non gérées sont utilisées, elles doivent être encapsulées dans une classe d’encapsulage IDisposable scellée qui implémente un finaliseur (comme SafeHandle ). Comme elle est scellée, cette classe n’a pas besoin du motif IDisposable complet.

  • Dans tous les autres cas, l’écrasante majorité, votre modèle plus simple pourrait être utilisé.

Mais à moins que Microsoft ou une autre source faisant autorité en fasse la promotion active, je continuerai à utiliser le modèle IDisposable complet.

Une autre option consiste à refactoriser votre code pour éviter l’inheritance et rendre vos classes IDisposable scellées. Ensuite, il est facile de justifier le modèle plus simple, car les girations maladroites pour supporter un inheritance éventuel ne sont plus nécessaires. Personnellement, je prends cette approche la plupart du temps; dans les rares cas où je souhaite créer une classe jetable non scellée, je ne fais que suivre le modèle «standard». Une bonne chose à propos de cultiver cette approche est qu’elle a tendance à vous pousser vers la composition plutôt que l’inheritance, ce qui rend généralement le code plus facile à maintenir et à tester.

Mon modèle Dispose recommandé consiste à implémenter une implémentation non virtuelle de Dispose en un void Dispose(bool) virtuel void Dispose(bool) virtuel, de préférence après quelque chose comme:

 int _disposed; public bool Disposed { return _disposed != 0; } void Dispose() { if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0) Dispose(true); GC.SuppressFinalize(this); // In case our object holds references to *managed* resources } 

En utilisant cette approche, vous vous assurez que Dispose(bool) n’est appelé qu’une fois, même si plusieurs threads essaient de l’appeler simultanément. Bien que de telles tentatives d’élimination simultanées soient rares (*), il est bon marché de se protéger contre elles; Si la classe de base ne fait pas ce qui est décrit ci-dessus, chaque classe dérivée doit avoir sa propre logique de garde redondante à double disposition et probablement un drapeau redondant.

(*) Certaines classes de communication qui sont pour la plupart mono-threadées et utilisent des E / S bloquantes permettent à Dispose d’être appelé depuis n’importe quel contexte de thread pour annuler une opération d’E / S bloquant son propre thread [Evidemment, Dispose ne peut pas être appelé à ce stade. thread, puisque ce thread ne peut rien faire tant qu’il est bloqué]. Il est tout à fait possible – et pas déraisonnable – que de tels objects, ou des objects qui les encapsulent, aient un fil extérieur qui tente de les Dispose comme un moyen d’annuler leur opération en cours au moment même où ils allaient être éliminés par leur principal object. fil. Les appels d’ Dispose simultanés sont probablement rares, mais leur possibilité n’indique aucun “problème de conception”, à condition que le code d’ Dispose puisse agir sur un appel et ignorer l’autre.