Remerciements
C’est quoi ?
Introduction du traducteur
A qui s’adresse cet article ?
Dispose, Finalisation et
gestion des ressources
Aperçu
Le nettoyage implicite
Le nettoyage explicite
Le modèle Dispose
Exemples d’implémentation
Le problème du versionnage
Dispose
Conception de classes
disposablesTravailler avec
des objets IDisposable
Le block using en C# et VB, la
sémantique C++
Finalisation
Les finaliseurs C#
Exemples d’utilisation du
modèle Dispose
SafeHandler
HandleCollector et stress
mémoire
HandleCollector
Add/Remove MemoryPressure
L'article d'origine a été rédigé par Joe Duffy qui fait partie de l'équipe qui développe la BCL chez Microsoft. Je le remercie de m’avoir autorisé à réaliser cette traduction provenant d’un article publié dans son blog :
http://www.bluebytesoftware.com/blog/PermaLink.aspx?guid=88e62cdf-5919-4ac7-bc33-20c06ae539ae
Ce texte est en partie collectif, vous y trouverez donc les interventions de gens comme Jeffrey Richter, Herb Sutter, Brian Grunkemeyer,Chris Brumme, Brad Abrams, Krzysztof Cwalina, Clemens Szyperski.
Vu leur pédigrée on est dans de bonnes mains.
Je n’aime pas parler le Van Dame surtout si son usage relève d’une sorte de pédanterie pseudo-moderniste. Être dans le vent est une ambition que je laisse volontiers aux feuilles mortes. Mais l’exercice délicat de la traduction est difficile et appelle d’autant plus au compromis que ce n’est pas mon métier.
Par exemple si je ne cherche pas à traduire handle par poignée comme on le rencontre parfois, un design pattern est pour moi clairement un modèle de conception voire un modèle de développement. Je parle de ressources gérées et non pas managées et le verbe anglais dispose se traduit par détruire, ce qui dans ce papier est très précisément son sens d’ailleurs.
Je n’ai pas toutefois de problème à inventer des néologismes tels finaliseur et sa petite famille, du moment que cela s’écrive sans "z" !
J’espère ne pas avoir fait de trop graves contresens, mais n’hésitez pas à me les signaler. Pour reprendre la formule consacrée, je suis le seul responsable des erreurs contenues dans ce texte.
J'ai particulièrement été intéressé par ce document car il s'agit d'un article de fond. Le sujet est certes moins "sexy" que les classes génériques, mais il correspond à ce genre de bagages qui me fascine toujours chez les véritables gourous: la connaissance intime des petits détails.
De temps à autres j’ai ajouté au texte d’origine une « Note du traducteur ». Il s’agira toujours d’une référence bibliographique où l’on peut trouver des définitions et des clarifications sur tel concept dont parle le texte où apparaît la note. Je ne me suis en aucune manière permis d'altérer le texte d'origine.
Je pense qu’un développeur débutant ne pourra pas suivre longtemps. Il est indispensable d’avoir un minimum de bagage en .NET ou dans un autre langage géré tel Java ou éventuellement en C++.
Par ailleurs le document n’est pas un cours, mais plutôt une capture de savoir-faire. En particulier il fait de nombreuses références croisées. Il est donc absolument normal qu’une première lecture vous laisse parfois perplexe. C’est le genre de document que l’on doit lire au moins deux fois pour espérer comprendre.
De plus il présuppose un minimum de connaissance sur le sujet et en particulier le fonctionnement du ramasse-miettes. Vous trouverez une bonne introduction dans l’article de Thomas Gil (Tom) sur DNG : http://www.dotnetguru.org/articles/GC/GC.html
Ce guide de développement ne se confine pas à .NET 1.1, mais s’intéresse largement à la version 2.0 pour laquelle les nouveautés sont nombreuses.
Afin de pouvoir héberger la CLR dans SQL Server 2005 sans lui faire perdre son niveau de fiabilité cinq neuf, Microsoft a du en effet développer de nouveaux concepts : SafeHandle, finaliseur critique, zone d’exécution limitée (CER), contrat de fiabilité…
Un bon article d’introduction peut être lu ici:
http://www.microsoft.com/france/msdn/securite/Reliability_default.mspx
Mais aussi dans le chapitre 4 du
livre best-seller de Patrick Smacchia :
D’autres informations utiles peuvent être trouvées (en anglais) un peu partout.
Je vais juste citer ce papier de Chris Brumme au sujet de la finalisation :
http://blogs.msdn.com/cbrumme/archive/2004/02/20/77460.aspx
Et un document récapitulatif sur la gestion des ressources trouvé sur le site GotDotNet :
http://www.gotdotnet.com/team/libraries/whitepapers/resourcemanagement/resourcemanagement.aspx
Maintenant place aux experts !
| Note du traducteur : C’est ici que débute le papier de Joe Duffy. Pour info, l’état d’une instance d’objet est l’ensemble des valeurs de ses champs de type valeur. |
Le ramasse-miettes réalise un travail extraordinaire dans la gestion des allocations mémoire des objets .NET, mais il n’a jamais été conçu pour prendre en charge la mémoire ou les ressources systèmes non gérées.
Il existe une différence considérable entre ces deux mondes au point de nécessiter un pont logiciel dont l’écriture est largement de la responsabilité des développeurs des API.
Le but principal lorsque l’on traite de telles ressource est très simplement de rendre le plus efficace possible leur utilisation. Plus particulièrement lorsque ces ressources sont limitées en quantité. Vous devez vous efforcer de fournir à vos utilisateurs les moyens d’acquérir et libérer les ressources dont ils ont besoin et mettre en place un filet de sécurité capable d’éviter tout risque de fuite de ces ressources.
Heureusement, .NET fournit une couche d’abstraction capable de masquer aux yeux des développeurs les détails relatifs aux ressources non gérées (handle Windows ou GDI, connexion à une base de données, SafeHandle…). Toutefois leur grande variété possible peut nous amener à prendre en charge nous même leur gestion.
Ce document récapitule les meilleures pratiques pour implémenter les mécanismes explicites ou implicites de nettoyage des ressources. On parle souvent de modèle de conception Dispose ou IDisposable car il implique normalement l’interface IDisposable et sa méthode Dispose pour le nettoyage explicite et éventuellement une méthode Finalize pour le nettoyage implicite.
Suivre correctement ce modèle est important si l’on souhaite garantir un nettoyage propre, opportun et standardisé des ressources allouées.
|
Krzysztof Cwalina: Beaucoup de gens qui entendent parler du modèle Dispose pour la première fois se plaignent que le ramasse-miettes ne fait pas son boulot. Ils pensent qu’il devrait collecter les ressources et que cela se passe exactement comme on le fait dans le monde non géré. En réalité le ramasse-miettes n’est absolument pas conçu pour prendre en charge les ressources, mais pour prendre en charge la libération de la mémoire. Cela il le fait très bien. |
Les types gérés peuvent à l’occasion envelopper des ressources qui non gérées. Dans de tels cas il est préférable d’utiliser la plus petite classe possible qui encadre de telles ressources. Idéalement on ne devrait n’y trouver que le code d’allocation, d’accès primaire et de libération. Il devient alors possible d’écrire une autre classe entièrement gérée afin de fournir un accès plus naturel. Le modèle de conception Dispose permet de limiter les risques et les difficultés qui seront examinés dans les chapitres qui suivent.
On doit toujours fournir une méthode explicite Dispose afin de libérer de façon déterministe les ressources appartenant à l’instance de la classe. Le nettoyage implicite par l’intermédiaire d’une méthode Finalize est également nécessaire lorsque la classe possède de telle ressource. Toutefois, souvent, la classe enveloppante (par exemple SafeHandle) s’en chargera pour vous. Reste alors au développeur la seule tâche d’implémenter le mécanisme de nettoyage explicite.
On devrait toujours fournir un nettoyage implicite pour protéger des ressources avec un SafeHandle. En fait, implémenter des finaliseurs à la main est rarement nécessaire depuis l’introduction du type SafeHandler dans l’environnement .NET 2.0.
Si une sémantique de finalisation supplémentaire est nécessaire, il est possible d’implémenter la méthode surchargée Finalize en utilisant la syntaxe spéciale mise à disposition par chaque langage (~T() en C# ou !T() en C++). L’environnement va alors pouvoir invoquer le finaliseur de façon non déterministe comme étape dans le processus de finalisation du ramasse-miettes afin de donner une dernière chance au système de libérer les ressources qu’il détient.
Non déterministe signifie simplement que la méthode sera invoquée par le ramasse-miettes à un moment indéterminé dans le temps, une fois qu’il n’existera plus de référence active à l’instance d’objet.
L’implémentation correcte d’un finaliseur est suffisamment difficile, nous verrons pourquoi plus loin, pour que vous deviez en étudier la nécessité.
Chaque fois qu’un type possède des ressources (ou des types qui eux même possèdent des ressources, vous devrez fournir à l’utilisateur un moyen de les libérer explicitement. Les développeurs disposent alors de l’option d’initier la libération des ressources lorsque l’objet n’est plus utilisé.
Vous allégez ainsi la tâche du ramasse-miettes ce qui ne peut qu’améliorer les performances et fournit en plus à l’utilisateur un moyen de réclamer les ressources allouées de façon déterministe.
De plus, si la ressource externe est rare ou coûteuse, comme par exemple les handles alloués par l’OS, les performances peuvent s’améliorer et éviter un épuisement de ces ressources.
Un contrôle explicite doit absolument se faire à l’aide de la méthode standard Dispose exposée depuis l’interface IDisposable - en C++, écrivez simplement un destructeur, ~T(), pour le type T, le compilateur génèrera la plomberie Dispose pour vous -.
Tout ceci implique en fait beaucoup plus de choses qu’écrire une méthode Dispose lorsque l’on créé une classe non scellée.
|
Clemens Szyperski : Le seul problème est que la
prise en charge automatique de la mémoire et des
objets rend difficile de garantir que les
ressources portées par les objets soient
libérées de façon déterministe, c’est à dire
rapidement. La confiance dans le ramasse-miettes
entraîne les développeurs à penser qu’ils n’ont
jamais à s’en soucier, ce qui n’est pas le cas.
En fait, tout objet qui implémente
IDisposable devrait être mentalement marqué
d’un drapeau rouge de façon à ne jamais être
autorisé à quitter le code actif sans que l’on
appelle sa méthode Dispose. Le filet de
sécurité finalisation/Safe handle n’est pas
suffisamment puissant pour que laisser les
développeurs produire du code sale – fichier qui
reste verrouillé pour une durée indéterminée
après un « save » ou un « close » sur le
document tandis que l’application continue de
tourner. L’utilisation prudente des domaines
d’application avec leur déchargement rude (qui
déclenche les Safe Handle) est parfois un moyen
de s’arranger avec cette rigueur. |
|
Herb Sutter : Les finaliseurs sont en fait pire que cela. A part le fait qu’ils se lancent tardivement (ce qui est en effet un sérieux problème pour beaucoup de type de ressource) ; ils sont de plus moins puissants parce que ils ne peuvent réaliser certaines opération permises à un destructeur (ex : un finaliseur ne peut utiliser un autre objet de façon fiable contrairement au destructeur). L’écriture elle même d’un finaliseur est extrêmement difficile. De plus la collection des objets finalisables par le ramasse-miettes est elle même très coûteuse : chaque objet finalisable – et le graphe potentiellement important d’objets atteignables depuis lui – est promu à la génération suivante du ramasse miette, ce qui accroît le coût de collection de plusieurs ordres de grandeur. Aujourd’hui, sur la plupart des ramasse-miettes dont celui de .NET, le bon conseil est : lorsque vous devez nettoyer des ressources, le code de nettoyage doit toujours être fournit sous la forme d’un destructeur (Dispose), jamais un finaliseur. Si vous écrivez un finaliseur, il faut également fournir un destructeur en plus et pas à la place de. |
|
Brian Grunkemeyer : Il y a deux concepts différents, mais qui parfois se confondent, concernant la destruction des objets.
C++ non géré implémente des destructeurs qui sont déclenchés de façon déterministe lorsque l’objet quitte la portée du code actif ou lorsque le développeur appelle une méthode delete sur le pointeur. Ceci met fin à l’existence de la ressource et, au moins dans le cas d’un delete, de l’objet portant cette ressource. La finalisation par la CLR ne permet que de lancer du code à la fin de vie de l’objet porteur. Se reposer sur la finalisation comme seul moyen de nettoyer les ressources fait que sa durée d’existence devient identique à celle de l’objet. Ceci peut provoquer des problèmes si l’accès à la ressource est exclusif et que celle-ci est en nombre limité. Les performances de l’application vont alors être altérées. Utilisez donc plutôt Dispose dans le code géré qui est le mécanisme le plus proche d’un destructeur en C++. Vous reléguez ainsi la finalisation au rang de sécurité ultime contre les utilisateurs qui négligent d’appeler Dispose. C’est le mieux compte tenu des restrictions d’utilisation sur les finaliseurs. Maintenant que vous comprenez bien cela, notez que Finalize et Dispose servent deux objectifs distincts. Malheureusement ils sont implémentés de façon diverses dans différents langages. C# désigne un finaliseur comme un destructeur, vous forçant ainsi à utiliser la syntaxe ~T(), tandis que Dispose a la forme d’une méthode normale. En C++, depuis la version .NET 2, Dispose est généré par le code à l’aide de la syntaxe ~T, tandis que le finaliseur réclame la syntaxe !T(). La syntaxe C# est indiscutablement critiquable en ce qu’elle favorise la confusion entre finalisation et destruction. Par conséquent lorsque vous lisez le mot « destructeur » ou bien ~T() dans une discussion au sujet de Dispose ou la durée de vie d’un objet, soyez attentif à l’intention exacte de l’auteur. |
En général, on considère qu’une bonne conception de la part d’un consommateur d’un objet qui implémente IDisposable est d’appeler Dispose lorsqu’il n’a plus besoin de l’objet. C’est facile dans un langage tel C# qui offre une syntaxe spécifique avec le block using. C++ qui automatise complètement l’utilisation du bock using.
Si votre classe n’est pas scellée et doit effectuer un nettoyage des ressources, il est indispensable de suivre précisément le modèle de conception qui suit.
Pour le classes scellées, ce modèle n’a pas besoin d’être suivit, c’est à dire que vous pouvez vous contenter d’implémenter Dispose et Finalize avec la méthode dite simple.
Même dans ce dernier cas, votre code doit toujours adhérer au guide de codage concernant l’implémentation de la logique interne de Dispose ou Finalize.
Ce modèle a été conçu pour garantir un nettoyage fiable et prévisible, empêcher les fuites temporaires de ressources (comme par exemple un appel à Dispose oublié) et, plus important, fournit un modèle standard, non ambiguë, pour les développeurs et les compilateurs. La description qui suit démontre une ligne de conduite sur quand et pourquoi implémenter un finaliseur et pourquoi certains types IDisposable n’en utilise pas. Enfin, nous discuterons des problèmes de versionnage des classes qui nécessitent un nettoyage, en particulier lorsque l’on introduit un nouveau type dans la hiérarchie.
|
Herb Sutter: On ne doit pas écrire un finaliseur si on peut l’éviter. En plus de problèmes déjà mentionnés, la présence d’un finaliseur rend l’objet plus coûteux à utiliser, même si le finaliseur n’est jamais appelé. Le surcoût lors de l’allocation d’un objet finalisable vient du fait que l’on doit l’ajouter également à une liste d’objets finalisables. Ce coût ne peut être évité même si l’objet supprime la finalisation immédiatement dans son constructeur. |
En C++, écrivez simplement le destructeur avec la syntaxe ~T() et le compilateur génèrera automatiquement toute la plomberie nécessaire que nous allons décrire plus loin dans ce chapitre. Dans les cas rares où vous souhaitez écrire vous-même un finaliseur avec !T(), le moyen recommandé est de mettre autant de code que possible dans le finaliseur (c’est à dire de code que le finaliseur peut effectivement prendre en charge - par exemple vous ne pouvez, de façon certaine, atteindre d’autres objets dans un finaliseur), placez le reste dans le destructeur et faites en sorte que celui-ci appelle explicitement le finaliseur.
· Implémentez toujours le modèle Dispose sur un type non scellé qui contient des ressources qui doivent être ou peuvent être explicitement libérées. (par exemple les handles Windows, les ressources non gérées…). Ce modèle produit un moyen standardisé à la disposition des développeurs pour détruire de façon déterministe les ressources possédées par l’objet. Il aide également les sous-classes à libérer les ressources de sa classe de base.
· Implémentez toujours complètement le modèle Dispose si les sous-classes sont IDisposable quand bien même la classe de base ne possède aucune ressource IDisposable. Cela permettra aux utilisateurs de la classe de base de coder un modèle Dispose dans de bonnes conditions. Un exemple intéressant est la classe System.IO.Stream. Bien qu’il s’agit d’une classe abstraite et ne possède pas à ce titre de ressources, la plupart de ses sous-classes oui. C’est pourquoi elle implémente le modèle Dispose.
Implémentez toujours IDisposable
uniquement sur une classe dont l’une des classes de sa
hiérarchie parente ne l’a pas déjà fait. La méthode
publique Dispose() doit être non virtuelle et
consister seulement en deux lignes : un appel à
Dispose(true) suivit d’un appel à
GC.SuppressFinalize(this).
L’appel à SuppressFinalize ne doit intervenir que
si celui à Dispose a réussi, par conséquent on ne
doit pas placer cet appel dans un block finally.
Les types qui héritent d’une autre classe qui suivent
déjà ce modèle peuvent et doivent réutiliser
l’implémentation existante de Dispose().
|
Brad Abrams: Nous avons beaucoup débattu au sujet de l’ordre relatif des appels dans la méthode publique Dispose(). Par exemple : public void Dispose(){
Dispose(true); Ou bien : public void Dispose(){
GC.SuppressFinalize(this); Nous avons finalement opté pour la première solution qui garantit que GC.SuppressFinalize() n’est appelé que si l’appel à Dispose() à réussi. |
|
Jeffrey Richter : J’ai moi-même beaucoup
hésité quant à l’ordre de ces deux lignes.
Initialement j’aurai plutôt fais le deuxième
choix. Je pensais que si Dispose lève une
exception, alors il lèvera la même exception
lorsque Finalize sera appelé, il n’y a
donc aucun bénéfice à générer une deuxième
exception. |
|
Brian Grunkemeyer : L’ordre est important pour donner au code de finalisation (dans Dispose(false)) une chance de nettoyer les ressources, même si certaines hypothèses faites à des niveaux plus élevés ne puissent plus être faites. Une méthode Dispose, doit garantir une libération correcte des ressources à la fin de son exécution. Mais si le code devant offrir cette garantie échoue, on se repose sur le finaliseur qui appelle Dispose(false). Cela peut entraîner la corruption et la perte d’une partie des données, mais à ce moment là le problème est inévitable. On s’assure qu’au moins on n’a pas de fuite de ressource. Le code de finalisation doit déjà s’accommoder même du cas des objets partiellement construits, par conséquent la charge supplémentaire de gérer les objets où Dispose(true) échoue ne devrait pas être très significative. |
· Toujours créer ou surcharger la méthode protégée Dispose(bool) pour prendre en charge toute la logique de nettoyage. Le nettoyage doit entièrement être effectué dans cette méthode – en tenant compte si nécessaire de l’argument de la méthode. Cet argument sera égal à false si la méthode est invoquée depuis le finaliseur.
|
Jeffrey Richter : L’idée est que Dispose(bool) sait s’il est appelé dans le cas d’un nettoyage explicite (l’argument vaut true) ou bien par le ramasse-miettes (l’argument vaut false). Cette distinction est utile car la méthode Dispose(bool) sait de façon certaine si elle peut exécuter du code utilisant des références vers des objets externes. Elle sait alors à coup sûr que ces objets n’ont pas déjà été finalisés ou bien disposés. |
|
Joe Duffy : Le commentaire de Jeff peut sembler un peu exagéré. Par exemple, ne peut-on pas accéder de façon fiable à une référence vers un type qui n’est pas finalisable ? La réponse est, oui, vous le pouvez. Mais uniquement si vous êtes certain qu’il ne s’appuie pas sur des états eux-mêmes finalisables. Cette confiance, doit s’appuyer sur une analyse de relations complexes directes ou indirectes entre objets. Très difficile à faire en pratique sans compter les difficultés de maintenance au fil des différentes versions du code. En conclusion, vous pouvez être sûr à 100% que ce n’est pas une bonne idée. |
· Toujours appeler Dispose(bool) de la classe de base (si elle existe) comme la dernière opération faite dans le code de votre implémentation de Dispose(bool). Il est important de ne pas modifier la valeur de l’argument booléen au cours de votre logique de nettoyage. Vous vous assurez ainsi que la méthode de la classe de base effectuera son nettoyage et que celui ci n’interviendra pas avant que la sous-classe a terminé son travail au cas où elle a besoin des ressources de la méthode de base.
·
Toujours implémenter un finaliseur si
votre objet est responsable du contrôle de la durée de
vie d’au moins une ressource qui ne dispose pas de son
propre finaliseur. Un type comme SafeHandle par
exemple dispose de son propre finaliseur.
Dans les autres cas, les développeurs vont souvent
négliger l’écriture d’un code garantissant l’exécution
d’une logique de nettoyage explicite.
Si votre classe de base a déjà surchargée Finalize
conformément à la forme correcte du modèle, vous ne
devez pas faire à nouveau cette surcharge car l’appel à
votre surcharge virtuelle Dispose(bool) se fera
automatiquement.
Lorsque vous implémentez un finaliseur, placez toute la logique de finalisation dans la méthode Dispose(bool). Votre méthode de finalisation ne fera qu’un simple appel virtuel à Dispose(false) et rien d’autre.
Comme on l’a noté plus haut, toute logique de codage incapable de s’exécuter dans un contexte de finalisation, doit être filtrée de sorte à ne pas être appelée si l’argument est à false.
|
Joe Duffy : Avoir plusieurs finaliseur dans une hiérarchie de classes qui se conforment au modèle peut se traduire en des appels redondants de la mécanique de nettoyage. Une méthode virtuelle de finalisation qui chaîne automatiquement vers base.Finalize() – précisément ce que le compilateur C# met en place par défaut – effectuera N appels virtuels à Dispose(bool), N étant le nombre de finaliseurs dans la hiérarchie. Il en est ainsi parce que Finalize est appelé virtuellement ce qui en retour appelle virtuellement Dispose(bool). Chacun chaînant sur sa classe de base. Tant que le code implémenté est résistant à des appels multiples, le seul problème ainsi créé est une charge supplémentaire qui pèsera subtilement sur les performances lors du parcours de la chaîne des appels virtuels. |
|
Herb Sutter : Ce qu’explique Joe au sujet de C# ne se produit pas en C++ parce que C++ génère lui-même la plomberie recommandée. Dispose(bool) est alors le seul point de chaînage vers la classe de base. |
Dispose doit être considéré comme un mot réservé de façon à standardiser ce modèle de et éviter les confusions entre développeurs et compilateurs. Certains langages comme C++ peuvent choisir d’implémenter automatiquement ce modèle sur certains types en ajoutant des méthodes comme ~T() ou !T() et encapsulant leur appel dans Dispose(bool).
Dans la majorité des cas, vous n’aurez pas besoin d’implémenter de méthode Finalize. Cet exemple démontre le cas le plus simple, par exemple la présence d’un type SafeHandle dont on doit organiser le nettoyage :
|
using System;
public class
SimpleCleanup : IDisposable // Liste de champs nécessitant un nettoyage
private
SafeHandle handle;
public
SimpleCleanup()
protected
virtual
void Dispose(bool
disposing)
if (disposing)
disposed =
true;
public
void Dispose() |
Considérez cet exemple comme le prototype de toute implémentation correcte du cas avec finaliseur. L’exemple montre également à quoi doit ressembler une classe qui introduit pour la première fois dans une hiérarchie une logique de nettoyage :
|
using System;
public class
ComplexCleanupBase : IDisposable private bool disposed = false; // pour détecter les appels redondants
public
ComplexCleanupBase()
protected
void Dispose(bool
disposing) // logique de nettoyage partagée
disposed =
true;
~ComplexCleanupBase()
public
void Dispose() |
Cet exemple montre une classe qui étend ComplexeCleanupBase et s’intègre dans le cycle de vie de Dispose et Finalize pour assurer un comportement correct au nettoyage :
|
public class
ComplexCleanupExtender : ComplexCleanupBase private bool disposed = false; // détection des appels redondants
public
ComplexCleanupExtender() :
base()
protected
override
void Dispose(bool
disposing) // logique de nettoyage des nouvelles ressources, partagée
disposed =
true;
base.Dispose(disposing); |
Notez que ComplexCleanupExtender ne ré implémente pas Dispose ni Finalize puisque la classe parent s’en charge déjà. L’implémentation fournie par la classe de base restera toujours valide dans le contexte des classes dérivées.
|
public ref
class ComplexCleanupBase private: bool disposed; public:
ComplexCleanupBase() : disposed(false) // implémentation implicite de IDisposable virtual ~ComplexCleanupBase() {
Console::WriteLine("Base::~dtor"); // logique non finalisable
this->!ComplexCleanupBase(); } virtual
!ComplexCleanupBase() { if
(!disposed) { }; |
La surcharge du comportement de Dispose est montrée juste après. Notez que le chaînage vers la classe de base s’effectue automatiquement :
|
public ref class ComplexCleanupExtender : ComplexCleanupBase { private: // champs supplémentaires nécessitant une logique de nettoyage bool disposed; public: ComplexCleanupExtender() : disposed(false) { //
allocation des ressources virtual ~ComplexCleanupExtender() { Console::WriteLine("Extender::~finalizer"); if (!disposed) { // logique non finalisable
this->!ComplexCleanupExtender(); virtual !ComplexCleanupExtender() { Console::WriteLine("Extender::!finalizer"); if
(!disposed) { |
Si vous choisissez d’ajouter ce modèle de conception à une classe existante et non scellée, vous pouvez perturber à votre insu des sous classes déjà existantes. Pour la même raison, changer de contrat de sémantique entre une classe de base et une classe dérivée peut soulever de subtils problèmes de compatibilité. Introduire une notion de « disposabilité » dans une classe de base alors que rien de tel n’existait auparavant est problématique.
De plus l’ajout de nouvelles méthodes à une classe de base peut générer de nouvelles alertes de compilation sur les sous classes existantes. Ce chapitre aborde les points que vous devez soigneusement étudier avant de prendre une décision.
|
Note du traducteur : On peut ou non avoir Dispose(), Dispose(bool) ou un finaliseur. Les trois premières colonnes de cette table forment la liste de toutes les combinaisons possibles. Les trois colonnes qui suivent montre ce qui doit être alors fait dans chaque cas. |
|
Classe de base |
Que faire si on dérive ? |
||||
|
Base expose Dispose ?
|
Base expose la méthode virtuelle Dispose(bool) ? |
Base a un finaliseur ?
|
IDisposable
|
Virtual Dispose(bool) |
Surcharge de Finalize
|
|
Non |
Non |
Non |
Implémenter un sealed public |
Si absente de la classe de base, alors la créer avec la portée protected, autrement la surcharger avec override |
Si surchargée, mais pas scellée dans la classe de base, alors re surcharger et sceller. |
|
Non |
Non |
Oui |
|||
|
Non |
Oui |
Non |
|||
|
Non |
Oui |
Oui |
|||
|
Publiquement |
Non |
Non |
Ré implémenter avec un sealed private |
||
|
Publiquement |
Non |
Oui |
|||
|
Non publiquement |
Non |
Non |
|||
|
Non publiquement |
Non |
Oui |
|||
|
Publiquement |
Oui |
Non |
Si la version de base n’est pas sealed, alors surchargez avec override sealed |
||
|
Publiquement |
Oui |
Oui |
|||
|
Non publiquement |
Oui |
Non |
|||
|
Non publiquement |
Oui |
Oui |
|||
|
Herb Sutter : Cette table est construite à partir de ce que génère le compilateur C++ automatiquement lorsqu’il détecte une classe de base qui ne suit pas le modèle de conception que nous avons présenté dans ce chapitre. |
|
Note du traducteur : Depuis .NET 2.0, le modificateur sealed s’applique également aux méthodes et aux propriétés virtuelles. Dans ce cas on doit y ajouter le modificateur override. |
Si un type implémente IDisposable et se conforme à l’un des modèles présentés précédemment, alors le guide de conception suivant s’applique. Notez que cela concerne tout code qui s’exécute à l’intérieur de Dispose(), Dispose(bool) ou tout autre méthode pouvant être appelée par Dispose.
|
Jeffrey Richter : Cette règle est très importante et ne devrait pas subir d’exception. Sans elle, un utilisateur du type ne peut contrôler correctement les ressources utilisées. |
|
Herb Sutter : Les langages lèvent généralement une alerte dans ce cas. Si vous avez un finaliseur, vous devez avoir un destructeur (Dispose). La seule exception sont les types par valeur car on ne peut avoir ni finaliseur, ni destructeur (la CLR effectue arbitrairement de nombreuses copies bit à bit, il devient alors impossible d’écrire un code de nettoyage correct dans ce cas). |
|
Brian Grunkemeyer : Une méthode Dispose(bool) peut être appelée plusieurs fois à cause du mécanisme de résurrection (c’est à dire quelqu’un qui a appelé GC.SuppressFinalize sur votre instance), ou parce que un programme vérifiable peut pour diverses raisons appeler plusieurs fois Dispose ou Finalize. Bien que rare et particulier, ceci n’a rien d’illégal. |
|
Herb Sutter : Malheureusement, appeler Dispose(bool) plusieurs fois n’est pas si étrange que cela. C’est ce que fait la chaîne de finalisation générée automatiquement par C#. C’est pourquoi il est important d’implémenter une seule fois Dispose(bool) dans la hiérarchie. |
|
Herb Sutter : En C++, vous pouvez simplement suivre la syntaxe naturelle qui consiste à stocker un type dont la durée de vie est contrôlée par votre propre type dans un type par valeur. Le nettoyage transitif sera alors fait automatiquement. Par exemple on écrira : ref class R { private: TextReader tr; // par valeur, non par référence (^) }; Ici R ::~R() va automatiquement appeler tr.~TextReader() suivant la syntaxe usuelle en C++. |
|
public class
CyclicClassA :
IDisposable
private
TextReader myReader;
public
void Dispose()
if
(myReader != null)
if
(cycle != null)
public class
CyclicClassB :
IDisposable
public
void Dispose()
if
(cycle != null) |
Dans cet exemple, étant donné a une instance de CyclicClassA et b une instance de CyclicClassB.
Si a.cycle = b et b.cycle = a, la destruction transitive devrait engendrer une boucle infinie. C’est pourquoi l’objet est mis à null pour éviter ce problème.
|
void NaiveConsumer() TextReader tr = new StreamReader(File.OpenRead("foo.txt"));
try // on fait quelque chose ici
} } |
Si Dispose pouvait lever une exception, la logique implémentée ensuite dans le bloc finally ne s’exécuterai jamais. Pour éviter ce problème, l’utilisateur serait obligé de placer un autre bloc try/finally dans le bloc finally ! Cela devient vite très compliqué à suivre.
De plus, si l’exécution de Dispose(false) provoque une exception, le processus se terminerait dans le contexte du finaliseur.
L’exemple suivant montre un exemple de ce mécanisme. Il est conçu non pas comme un modèle de conception, mais comme un simple exemple :
|
class
Recreatable : IDisposable
public
void Dispose() // implémentation de Dispose(bool)… // Chaque fois que vous appelez une méthode qui accède aux ressources de cette classe, // vérifiez si son état disposed est à true. // dans ce cas, il faut réouvrir la ressource
public
void DoStuff()
// fait
le boulot
// Note:
public
void ReOpen() GC.ReRegisterForFinalization(this); handle = //obtient un nouveau handle otherRes = new OtherResource(); } } |
Notez qu’un scénario justifiant une déviation est le cas d’un objet qui peut être ouvert et fermé plusieurs fois sans que l’on souhaite recréer une instance. C’est par exemple le cas de System.Data.SqlClient.SqlConnection. Il est possible d’ouvrir et de fermer une connexion vers une base de données plusieurs fois.
· Toujours détruire une instance d’un objet IDisposable lorsque l’on en a plus besoin. Cette règle est partiellement couverte par une précédente qui statue que chaque objet doit transitivement détruire les champs IDisposable qu’il possède, mais aussi prendre en considération les allocations locales d’objets. Vous devez détruire prioritairement les variables locales dont aucune référence ne sort du bloc de code dans lequel elles ont été instanciées. Le moyen le plus simple d’obtenir cela en C# est d’utiliser un block using. En C++ allouez simplement un objet sur la pile.
Soyez attentif, toutefois, à ne pas détruire un objet encore utilisé. Contrairement à la finalisation, il est très facile d’accidentellement nettoyer une ressources sur un objet encore actif.
· Ne pas masquer les exceptions qui pourraient être levées dans une méthode Dispose. L’invocation d’une exception lors de l’appel à Dispose ne se produit qu’au cours de situations critiques. Dans de tels cas, il ne serait pas très prudent de la capturer et continuer l’exécution.
C# et VB proposent le block d’instruction using tandis que C++ offre une sémantique d’allocation dans la pile afin de faciliter le travail des développeurs utilisant des objets IDisposable. L’objectif de ces outils est de définir une portée au delà de laquelle l’objet est automatiquement détruit. Cette action est indépendante du fait que le flux de code soit normal ou résulte d’une exception.
En C# et en VB, using est adapté au portées locales où la durée de vie d’un objet s’étend sur un block de code facilement identifiable. Pour des portées plus larges, comme par exemple les champs IDisposable, il est préférable d’invoquer directement Dispose. En C++, déclarer un champ par valeur est encore la meilleure solution. Le destructeur du champ sera alors appelé automatiquement lorsque l’objet conteneur sera détruit que ce soit dans le cours normal de l’exécution ou à la suite d’une exception.
L’exemple suivant est en C# :
|
void UseDisposableObject() |
Ce qui s’écrirait en C++ :
|
void UseDisposableObject() { Resource r;
// utilise la ressource } |
Le code IL est l’équivalent du code suivant :
|
void UseDisposableObject()
try
|
Observez comment l’instruction using organise le nettoyage du code ce qui rend l’implémentation de Dispose plus attrayante pour les développeurs. Il est également possible d’empiler les instructions using comme par exemple :
|
using (Resource r1 =
new
Resource()) |
Et en C++ :
|
{ Resource r1; Resource r2;
// utilise r1 et r2 } |
|
Note du traducteur : L’instruction using est une des nouveautés de VB.NET. La syntaxe est la suivante :
Using resource As New resourceType ' utilise la ressource End Using |
Si vous implémentez une logique de finalisation, il est important de le faire correctement. Les finaliseurs sont notoirement diffic