IDisposable - que votre maman n'a pas parlé de libérer des ressources. Partie 1

Ceci est une traduction de la première partie de l'article. L'article a été écrit en 2008. Après 10 ans, presque perdu sa pertinence.


Libération déterministe des ressources - Un besoin


Au cours de plus de 20 ans d'expérience en codage, j'ai parfois développé mes propres langages pour résoudre des problèmes. Ils allaient de langages simples et impératifs à des expressions régulières spécialisées pour les arbres. Lors de la création de langues, il existe de nombreuses recommandations et certaines règles simples ne doivent pas être violées. L'un d'eux:


Ne créez jamais un langage d'exception dans lequel il n'y a pas de libération déterministe des ressources.

Devinez quelles recommandations le runtime .NET ne suit pas, et par conséquent, toutes les langues basées sur lui?


La raison pour laquelle cette règle existe est que la libération déterministe des ressources est nécessaire pour créer des programmes pris en charge . La libération déterminée des ressources fournit un certain point auquel le programmeur est sûr que la ressource est libérée. Il existe deux façons d'écrire des programmes fiables: l'approche traditionnelle consiste à libérer les ressources le plus tôt possible et l'approche moderne consiste à libérer les ressources pour une durée indéterminée. L'avantage de l'approche moderne est que le programmeur n'a pas besoin de libérer explicitement des ressources. L'inconvénient est qu'il est beaucoup plus difficile d'écrire une application fiable, il y a beaucoup d'erreurs subtiles. Malheureusement, le runtime .NET a été créé en utilisant une approche moderne.


.NET prend en charge la libération non déterministe des ressources à l'aide de la méthode Finalize , qui a une signification particulière. Pour la libération déterministe des ressources, Microsoft a également ajouté l'interface IDisposable (et d'autres classes, dont nous discuterons plus tard). Néanmoins, pour l'exécution, IDisposable est une interface normale, comme tout le monde. Ce statut de "second ordre" crée certaines difficultés.


En C #, la "libération déterministe pour les pauvres" peut être implémentée en utilisant les try et finally ou en using (ce qui est presque la même chose). Microsoft discute depuis longtemps de la nécessité ou non de compter les liens, et il me semble que la mauvaise décision a été prise. Par conséquent, pour une libération déterministe des ressources, vous devez utiliser les constructions maladroites finally \ using ou un appel direct à IDisposable.Dispose , qui est lourd d'erreurs. Pour un programmeur C ++ habitué à utiliser shared_ptr<T> deux options ne sont pas attrayantes. (La dernière phrase indique clairement où l'auteur a une telle relation - env.


IDisposable


IDisposable est une solution de libération déterministe des ressources offerte par Misoftro. L'un concerne les cas suivants:


  • Tout type possédant des ressources gérées ( IDisposable ). Un type doit nécessairement posséder , c'est-à-dire gérer le temps de vie, les ressources, et pas seulement s'y référer.
  • Tout type possédant des ressources non gérées.
  • Tout type possédant à la fois des ressources gérées et non gérées.
  • Tout type hérité d'une classe qui implémente IDisposable . Je ne recommande pas d'hériter des classes qui possèdent des ressources non managées. Mieux vaut utiliser une pièce jointe.

IDisposable aide à libérer des ressources de façon déterministe, mais a ses propres problèmes.


Difficultés IDisposable - Facilité d'utilisation


IDisposable objets IDisposable sont IDisposable utiliser assez encombrants. L'utilisation d'un objet doit être encapsulée dans une construction using . La mauvaise nouvelle est que C # ne permet pas d'utiliser using avec un type qui IDisposable pas IDisposable . Par conséquent, le programmeur doit se référer à la documentation à chaque fois pour comprendre s'il est nécessaire d'écrire en using , ou simplement d'écrire en using partout, puis d'effacer où le compilateur jure.


Le C ++ managé est bien meilleur à cet égard. Il prend en charge la sémantique de pile pour les types de référence , qui fonctionne comme using que les types lorsque cela est nécessaire. C # pourrait bénéficier de la possibilité d'écrire en using n'importe quel type.


Ce problème peut être résolu avec. outils d'analyse de code. Pour aggraver les choses, si vous oubliez d'utiliser, le programme peut passer les tests, mais planter en travaillant "dans les champs".


Au lieu de compter les liens, IDisposable a un autre problème - déterminer le propriétaire. Quand en C ++ la dernière copie de shared_ptr<T> sort du domaine d'application, les ressources sont libérées immédiatement, pas besoin de penser à qui doit être libéré. IDisposable au contraire, oblige le programmeur à déterminer qui "possède" l'objet et qui est responsable de sa libération. Parfois, la propriété est évidente: lorsqu'un objet en encapsule un autre et implémente lui-même IDisposable , il est donc responsable de la libération des objets enfants. Parfois, la durée de vie d'un objet est déterminée par un bloc de code, et le programmeur utilise simplement l'utilisation autour de ce bloc. Néanmoins, il existe de nombreux cas où un objet peut être utilisé à plusieurs endroits et sa durée de vie est difficile à déterminer (bien que dans ce cas, le décompte de référence ferait très bien l'affaire).


Difficultés IDisposable - Compatibilité descendante


L'ajout d' IDisposable à la classe et la suppression d' IDisposable de la liste des interfaces implémentées est un changement de rupture. Le code client qui n'attend pas IDisposable ne libérera pas de ressources si vous ajoutez IDisposable à l'une de vos classes transmises par référence à une interface ou une classe de base.


Microsoft lui-même a rencontré ce problème. IEnumerator pas hérité d' IDisposable et IEnumerator<T> hérité. Si vous passez IEnumerator<T> code qui reçoit IEnumerator , Dispose ne sera pas appelé.


Ce n'est pas la fin du monde, mais cela donne une essence secondaire d' IDisposable .


Difficultés IDisposable - Conception d'une hiérarchie de classes


Le plus gros inconvénient causé par IDisposable dans le domaine de la conception de hiérarchie est que chaque classe et interface doit prédire si IDisposable sera nécessaire à ses descendants.


Si l'interface n'hérite pas d' IDisposable , mais que les classes implémentant l'interface implémentent également IDisposable , le code final ignorera la version déterministe ou devra vérifier si l'objet implémente l'interface IDisposable . Mais pour cela, il ne sera pas possible d'utiliser la construction using et vous devrez écrire un try laid et finally .


Bref, IDisposable complique le développement de logiciels réutilisables. La raison principale est la violation de l'un des principes de la conception orientée objet - séparation de l'interface et de la mise en œuvre. La libération des ressources devrait être un détail de mise en œuvre. Microsoft a décidé de faire de la libération déterministe des ressources une interface de deuxième classe.


L'une des solutions les moins belles est de faire en sorte que toutes les classes implémentent IDisposable , mais dans la grande majorité des classes, IDisposable.Dispose ne fera rien. Mais ce n'est pas trop beau.


Une autre difficulté avec IDisposable est les collections. Certaines collections y «possèdent» des objets, d'autres non. Cependant, les collections elles-mêmes IDisposable pas IDisposable . Le programmeur doit se rappeler d'appeler IDisposable.Dispose sur les objets de la collection ou de créer ses propres descendants de classes de collection qui implémentent IDisposable pour signifier la propriété.


Difficultés IDisposable - état "erroné" supplémentaire


IDisposable peut être appelé explicitement à tout moment, quelle que soit la durée de vie de l'objet. Autrement dit, un état «libéré» est ajouté à chaque objet, dans lequel il est recommandé de ObjectDisposedException une ObjectDisposedException . Vérifier l'état et lever les exceptions est une dépense supplémentaire.


Au lieu de vérifier chaque éternuement, il est préférable d'envisager d'accéder à l'objet à l'état «libéré» comme un «comportement non défini» comme un appel à la mémoire libérée.


Difficultés IDisposable - aucune garantie


IDisposable n'est qu'une interface. Une classe qui implémente IDisposable prend en charge la version déterministe, mais ne la garantit pas. Pour le code client, il est préférable de ne pas appeler Dispose . Par conséquent, une classe qui implémente IDisposable doit prendre en charge la version déterministe et non déterministe.


Complexités IDisposable - Complex Implementation


Microsoft propose un modèle pour implémenter IDisposable . (Auparavant, il y avait un schéma généralement terrible, mais relativement récemment, après l'apparition de .NET 4, la documentation a été corrigée, y compris sous l'influence de cet article. Dans les anciennes éditions de livres .NET, vous pouvez trouver l'ancienne version. - environ. )


  • IDisposable.Dispose ne peut pas être appelé du tout, donc la classe doit inclure un finaliseur pour libérer des ressources.
  • IDisposable.Dispose peut être appelé plusieurs fois et devrait fonctionner sans effets secondaires visibles. Par conséquent, il est nécessaire d'ajouter une vérification si la méthode a déjà été appelée ou non.
  • Les finaliseurs sont appelés dans un thread séparé et peuvent être appelés avant la IDisposable.Dispose . L'utilisation de GC.SuppressFinalize pour éviter de telles "races".

De plus:


  • Les finaliseurs sont appelés, y compris pour les objets qui lèvent une exception dans le constructeur. Par conséquent, le code de version doit fonctionner avec des objets partiellement initialisés.
  • L'implémentation d'un IDisposable dans une classe héritée de CriticalFinalizerObject nécessite des constructions non triviales. void Dispose(bool disposing) est une méthode virale et doit être exécutée dans la région d'exécution contrainte , ce qui nécessite un appel à RuntimeHelpers.PrepareMethod .

Difficultés IDisposable - Ne convient pas pour la logique d'achèvement


Arrêter un objet - se produit souvent dans des programmes en threads parallèles ou asynchrones. Par exemple, une classe utilise un thread distinct et souhaite le terminer à l'aide de ManualResetEvent . Cela peut être fait dans IDisposable.Dispose , mais cela peut entraîner une erreur si le code est appelé dans le finaliseur.


Pour comprendre les limitations du finaliseur, vous devez comprendre le fonctionnement du garbage collector. Vous trouverez ci-dessous un diagramme simplifié dans lequel de nombreux détails liés aux générations, aux maillons faibles, à la renaissance des objets, au ramasse-miettes, etc. sont omis.


Le garbage collector .NET utilise l'algorithme de marquage et de balayage. En général, la logique ressemble à ceci:


  1. Mettez en pause tous les threads.
  2. Prenez tous les objets racine: variables sur la pile, champs statiques, objets GCHandle , file d'attente de finalisation. Dans le cas du déchargement du domaine d'application (arrêt du programme), on considère que les variables dans la pile et les champs statiques ne sont pas des racines.
  3. Parcourez récursivement tous les liens des objets et marquez-les comme «accessibles».
  4. Parcourez tous les autres objets qui ont des destructeurs (finaliseurs), déclarez-les accessibles et placez-les dans la file d'attente de finalisation ( GC.SuppressFinalize indique au GC de ne pas le faire). Les objets sont mis en file d'attente dans un ordre imprévisible.

En arrière plan, un flux (ou plusieurs) de finalisation fonctionne:


  1. Prend un objet de la file d'attente et démarre son finaliseur. Il est possible d'exécuter plusieurs finaliseurs d'objets différents en même temps.
  2. L'objet est supprimé de la file d'attente et si personne d'autre n'y fait référence, il sera effacé lors de la prochaine récupération de place.

Maintenant, il devrait être clair pourquoi il est impossible d'accéder aux ressources gérées depuis le finaliseur - vous ne savez pas dans quel ordre les finaliseurs sont appelés. Même appeler IDisposable.Dispose un autre objet à partir du finaliseur peut entraîner une erreur, car le code de libération des ressources peut fonctionner dans un autre thread.


Il existe quelques exceptions lorsque vous pouvez accéder aux ressources gérées à partir d'un finaliseur:


  1. La finalisation des objets hérités de CriticalFinalizerObject est effectuée après la finalisation des objets non hérités de cette classe. Cela signifie que vous pouvez appeler ManualResetEvent depuis le finaliseur jusqu'à ce que la classe soit héritée de CriticalFinalizerObject
  2. Certains objets et méthodes sont spéciaux, comme la console et certaines méthodes de thread. Ils peuvent être appelés depuis les finaliseurs même si le programme se termine.

Dans le cas général, il est préférable de ne pas accéder aux ressources gérées depuis les finaliseurs. Néanmoins, la logique de complétion est nécessaire pour les logiciels non triviaux. Sur Windows.Forms contient la logique de complétion dans la méthode Application.Exit . Lorsque vous développez votre bibliothèque de composants, la meilleure chose à faire est de compléter la logique de complétion avec IDisposable . Terminaison normale en cas d'appel d' IDisposable.Dispose et urgence sinon.


Microsoft a également rencontré ce problème. La classe StreamWriter possède un objet Stream (en fonction des paramètres du constructeur dans la dernière version - environ Per. ). StreamWriter.Close vide le tampon et appelle Stream.Close (se produit également s'il est Stream.Close en using - environ Per. ). Si StreamWriter pas fermé, le tampon n'est pas vidé et la conversation de données est perdue. Microsoft n'a tout simplement pas redéfini le finaliseur, «résolvant» ainsi le problème d'achèvement. Un excellent exemple de la nécessité d'une logique d'achèvement.


Je recommande la lecture


De nombreuses informations sur les composants internes .NET dans cet article proviennent du CLR de Jeffrey Richter via C #. Si vous ne l'avez pas encore, achetez-le . Sérieusement. C'est la connaissance nécessaire pour tout programmeur C #.


Conclusion du traducteur


La plupart des programmeurs .NET ne rencontreront jamais les problèmes décrits dans cet article. .NET évoluera pour augmenter le niveau d'abstraction et réduire le besoin de "jongler" avec les ressources non gérées. Néanmoins, cet article est utile en ce qu'il décrit les détails profonds de choses simples et leur impact sur la conception du code.


La prochaine partie sera une discussion détaillée sur la façon de travailler avec des ressources gérées et non gérées dans .NET avec un tas d'exemples.

Source: https://habr.com/ru/post/fr414873/


All Articles