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 difficiles à implémenter essentiellement parce que l’on ne peut faire certaines hypothèses (normalement valides) au sujet de l’état du système. Le guide de développement qui suit vous aidera dans cette tâche.
Ce guide ne concerne pas seulement la méthode Finalize (C# : ~T(), C++ : !T()), mais à tout code qui s’exécute durant la finalisation. Dans le cadre du modèle Dispose défini précédemment, il s’agit du code exécuté dans Dispose(bool) lorsque l’argument vaut false.
· Analysez avec soin tous les cas où vous pensez avoir besoin d’un finaliseur. Il existe un coût réel associé à une instance d’un objet exposant un finaliseur, tant du point de vue des performances que de la complexité du code. Préférez l’utilisation de conteneurs tels SafeHandle chaque fois que possible. Dans ce cas le finaliseur est inutile car le conteneur est responsable de la libération de ses ressources.
La finalisation augmente le coût et la durée de vie de chaque objet finalisable car ils doivent être enregistrés dans une pile spécialisée lors de leur allocation. Essentiellement un pointeur supplémentaire est créé vers cet objet.
De plus, les objets dans la file sont parcourus durant la collection par le ramasse-miettes, traité et éventuellement promus vers une autre file utilisé par le ramasse-miettes afin d’invoquer le finaliseur. L’augmentation du nombre d’objets finalisables accroît mécaniquement le nombre d’objet promus vers les générations supérieures et augmente donc le temps passé par le ramasse-miettes à parcourir cette file, déplacer les pointeurs et lancer le code de finalisation. De plus, en conservant des objets plus longtemps, on augmente le temps durant lequel ils mobilisent de la mémoire.
· Ecrivez des méthodes Finalize protected, ni public ni private. Les développeurs C# et C++ n’ont en fait pas à s’en soucier car le compilateur le fera pour eux automatiquement.
· La méthode Finalize ne doit libérer que les ressources non gérées détenue par la classe conteneur. Ne touchez aucun objet finalisable dont votre objet pourrait avoir une référence car vous ne savez pas s’il a déjà été finalisé. Même les objets gérés dont vous contrôlez le cycle de vie peuvent être dans un état indéfini.
Par exemple, un objet finalisable A possédant une référence vers un autre objet finalisable B ne peut de façon fiable utiliser B dans un finaliseur et vice-versa. Il n’y a pas de notion d’ordonnancement dans les finaliseurs. De plus, les objets stockés dans des variables statiques seront collectés à un certain moment durant le déchargement du domaine d’application ou bien la fin du processus. Accéder à une variable statique qui référence un objet finalisable (ou bien appeler une méthode statique qui pourrait utiliser des valeurs stockées dans des variables statiques, comme par exemple les fonctions de trace) n’est pas sain, même s’il existe Environment.HasShutdownStarted (depuis la 1.1) qui peut détecter si le finaliseur s’exécute durant le déchargement d’un domaine d’application ou bien pendant l’interruption d’un processus.
|
Jeffrey Richter : Remarquez qu’il est correct d’atteindre des types par valeur décompartimentées (unboxed). |
Voici des exemples où un code de nettoyage pourrait être exécuté plusieurs fois :
|
Brian Grunkemeyer : Votre finaliseur peut parfaitement s’exécuter en même temps que des méthodes d’instances de votre objet. Si le finaliseur ferme une ressource utilisée par votre type, on peut alors devoir appeler GC.KeepAlive(this) à la fin de chaque méthode d’instance qui ne se sert pas du pointeur this après avoir effectuée quelques opération sur cette ressource. Si vous pouvez utiliser SafeHandle pour encadre la ressource, vous pouvez presque toujours supprimer le finaliseur de votre type, vous n’avez alors plus besoin de vous soucier de ce problème. |
|
Note du traducteur : Vous trouverez des informations concernant GC.KeepAlive() et le problème mentionné plus haut ici : http://www.dotnetguru2.org/amethyste/index.php?p=206&more=1&c=1&tb=1&pb=1 |
La question de savoir quand et comment cela peut arriver varie selon que l’on exécute du code dans un environnement hébergé ou non.
Dans le cas d’un code non hébergé, la finalisation peut ne jamais être lancée, par exemple:
- Si l’arrêt d’un processus provoque un dépassement de délai (time-out)
- Si un thread de finalisation est interrompu après avoir retiré l’objet de la pile de finalisation, mais avant d’avoir appelé le finaliseur. Dans ce cas .NET passe à l’objet suivant dans la pile.
- Si un processus est arrêté sans passer par une interruption gérée (appel par P/Invoke à kernel32!ExitProcess).
Tout cela peut également se produire dans le cas d’un code hébergé, mais l’hôte peut ultérieurement initier un déchargement rude du domaine d’application. Dans ce cas, seuls les finaliseurs critiques seront appelés.
Par exemple dans le cas d’un code hébergé par SQLServer, un déchargement normal d’un domaine d’application peut se transformer en déchargement rude si un thread ou bien un finaliseur ne répond pas au bout d’un certain temps.
Même en dehors de telles situations, un objet finalisable avec une référence publiquement accessible peut avoir sa finalisation court-circuitée par n’importe quel code client non fiable (untrusted). Plus spécialement il peut appeler GC.SuppressFinalizable et empêcher ainsi la finalisation de se faire, y compris les finalisations critiques. Une bonne stratégie pour diminuer les risques est d’envelopper les ressources critiques dans une instance non publique qui expose un finaliseur. Tant que cette instance n’est pas passée au code appelant, il ne pourra pas annuler sa finalisation. Si vous utilisez SafeHandle dans votre code et ne l’exposez jamais au monde extérieur, vous pouvez garantir que la finalisation de la ressource sera faite (sous réserve, comme mentionné plus haut que l’implémentation du SafeHandle soit correcte).
|
Note du traducteur : CriticalFinalizerObject et la notion de finalisation critique est une nouveauté de .NET 2.0. On peut trouver des informations ici : Jusqu’à la version .NET 1.1, il n’existait aucun mécanisme garantissant la finalisation d’un objet. Cela pouvait poser des problèmes pour les applications à très haute disponibilité. La finalisation critique fournit un moyen de répondre à ce problème. |
|
Note du traducteur : La notion de zone d’exécution limitée (constrained execution region) est aussi une des nouveautés de .NET 2.0. Un article en français expliquant cette notion a été donnée dans le chapitre d’introduction. |
L’exemple C# qui suit montre un appel dynamique à Dispose.
L’auteur de la classe de base s’attend à voir s’exécuter la méthode Dispose de la classe de base (c’est à dire que chaque sous-classe chaînera en appelant base.Dispose comme se serait le cas automatiquement en C++). En fait la version dérivée de Dispose ne fait jamais cela. Les ressources de la classe de base ne sont donc jamais libérées et entraîne une fuite de mémoire.
Si la méthode Dispose() dérivée annule la finalisation, les ressources détenues par la classe de base ne seront pas libérées, pas même lors de la finalisation !
|
public class
Base : IDisposable
~Base()
public class
Derived : Base
~Derived()
// BUG: on ne doit pas surcharger un
finaliseur |
Parmi les solutions possibles à ce problème on pourrait rendre Dispose non virtuel, injecter une méthode privée DisposImpl destinée à appeler base.Dispose() que le finaliseur appellerait explicitement.
Il y a en effet un risque avec la méthode Dispose(Bool) du modèle Dispose que les sous-classes ne chaînent pas correctement avec sa classe de base.
|
Brian Grunkemeyer :
Avec le code précédent, le finaliseur de la classe dérivée sera exécuté et appelle la méthode Dispose() de la classe dérivée. Le compilateur C# ajoute en plus un bloc try/finally à chaque finaliseur qui appelle le finaliseur de la classe de base. De cette façon, le finaliseur dérivé appellera le finaliseur de sa classe de base qui va virtuellement appeler Dispose. Le finaliseur dérivé sera donc appelé une seconde fois. Cet appel redondant illustre bien la raison pour laquelle une classe ne doit avoir qu’un seul finaliseur. |
Considérez le code suivant dans lequel list peut être null si le constructeur lève une exception avant de l’avoir assigné :
|
public class
MyClass
public
MyClass()
~MyClass()
//bug: list peut être null |
On peut modifier ainsi le finaliseur afin d’éviter une NullReferenceException sur le thread de finalisation (qui entraîne la fermeture de l’application) :
|
~MyClass() //
version corrigée
|
|
Jeffrey Richter : Si un constructeur lève une exception, la CLR appelera quand même le finaliseur de l’objet. Par conséquent, les champs de l’objet peuvent ne pas être initialisés. Votre finaliseur doit prendre en compte ce type de problème. |
|
Chris Brumme : J’ai décris l’environnement d’exécution des threads lors de la finalisation ici : http://blogs.msdn.com/cbrumme/archive/2004/02/20/77460.aspx Comme vous pouvez le constater, les techniques comme la résurrection peuvent faire que les threads applicatifs ou le thread de finalisation accèdent à votre objet de façon concurrente. En d’autres termes, du point de vue de la sécurité vous devez supposer un multi-threading massif. D’un autre côté vous devez aussi supposer qu’un seul thread sera actif dans votre objet lorsque vous le finaliserez ou le détruirez. Vous êtes libre d’ignorer cette possibilité aussi longtemps que cela ne créé pas de trous de sécurité. Dans ce cas vous devrez corriger votre code. Un moyen évident est de rendre la finalisation thread-safe.
StringBuilder est un excellent exemple. Si vous utilisez StringBuilder depuis plusieurs threads, le texte placé dans le buffer peut être collecté par le ramasse-miettes. C’est le problème du code client de l’objet. Ce problème de multi-threading exposait les premières versions de StringBuilder à un trou de sécurité. Il était alors possible de créer des chaînes mutables dans lesquelles le résultat de StringBuilder.ToString() pouvait être modifié. C’est un grave problème de sécurité et il a été corrigé depuis. Toutefois le problème d’intégrité des données n’est pas un trou de sécurité et ne sera jamais corrigé. Considérez le même genre de distinction avant de rendre ou non thread-safe vos finaliseurs. |
|
Brian Grunkemeyer : La CLR pourrait choisir d’utiliser un jour plusieurs threads de finalisation et il se pourrait aussi qu’ils soient extraient du pool de thread ou même que ce soit votre thread principal. On a besoin de la liberté de lancer un finaliseur sur n’importe quel thread. |
|
Note du traducteur : Un exemple concret de ce problème a été abordé par Nicole Calinoiu, une toute nouvelle MVP sécurité. Dans certains cas l’exécution d’un finaliseur peut lever une exception de sécurité : http://spaces.msn.com/calinoiu/Blog/cns!1pYmj2Kpn4Oz9CW9IKwXQF8A!154.entry |
|
Note du traducteur : Lorsqu’un objet réclame sa finalisation il est considéré comme mort. Plus tard le ramasse-miettes le réactive afin d’exécuter sa méthode Finalize, une fois celle-ci exécutée l’objet est considéré comme (définitivement) mort à nouveau. C’est ce phénomène de réactivation qui est appelé résurrection. Ce dont on parle ici est le cas où le finaliseur place un pointeur de l’objet ressuscité vers une variable static comme par exemple :
class SomeType
class Application Vous trouverez plus d’information dans le livre de Jeffrey Richter d’où cet exemple est tiré : Une application possible de la résurrection est justement les pools d’objets. |
|
Rico Mariani : Ce n’est pas un problème spécialement différent des autres problèmes liés à la finalisation. Si vous recyclez des ressources non gérées vous pouvez avoir déjà besoin d’un finaliseur. L’endroit où vous pouvez recycler est la méthode Dispose().
Lisez l’article suivant (Object : Release or Recycle ?) au chapitre « Option Two » : http://blogs.msdn.com/ricom/archive/2004/02/11/71143.aspx Supposer que la finalisation est un événement rare peut être une bonne approximation : http://weblogs.asp.net/ricom/archive/2003/12/04/41281.aspx Ce guide de développement n’a pas pour intention d’interdire les pools d’objet, mais plutôt de faire en sorte que vous avez réfléchi avec soin à votre stratégie de recyclage. |
|
Brian Grunkemeyer : Considérez le thread de finalisation plutôt comme un thread extrait d’un pool – si vous le cassez en polluant son état, vous l’assumez. |
|
Brian Grunkemeyer : Au tout début de l’implémentation de .NET 1.0, je déboguais un problème dans un finaliseur qui ne semblait pas être appelé. Pour m’aider à comprendre s’il était lancé ou non j’y ai placé un appel à Console.WriteLine. Résultat l’application a explosée aussitôt avec un ObjectDisposedException. Comment un simple Console.WriteLine dans un finaliseur peut produire un tel problème ?
En fait le flux
sous-jacent à la console avait été finalisé
avant mon instance. J’ai retenu cette
leçon : utiliser uniquement des instances
non finalisables dans un finaliseur.
Retenez donc : l’utilisation de Console dans un finaliseur est maintenant fiable, mais aucune garantie pour les autres classes. |
La méthode Finalize est héritée de la classe System.Object, bien que seules celles qui la redéfinissent soient sujet à être finalisées. C# dispose d’une syntaxe spéciale qui simplifie l’écriture d’un finaliseur et en fait vous empêche de surcharger Finalize comme on le ferait pour une méthode ordinaire. Par exemple :
|
public class
Resource |
Sera transformé par le compilateur en ceci :
|
public class
Resource |
Ce dernier code ne se compilera pas, c’est la syntaxe standard qu’il convient d’utiliser.
|
Joe Duffy : Dans les premiers temps de .NET, les finaliseurs étaient appelés explicitement destructeurs par les programmeurs C#. L’expérience aidant, nous essayons maintenant de promouvoir l’idée qu’en réalité Dispose est bien plus proche d’un destructeur au sens de C++ (déterministe), tandis que le finaliseur est quelque chose d’autre (non déterministe). Le fait que C# adopte la syntaxe C++ est certainement une source de confusion. Cette confusion a été dommageable et maintenant nous faisons une claire distinction entre les ressources et durée de vie. |
|
Jeffrey Richter : Il est malencontreux que les concepteurs de C# aient choisis la syntaxe en tilde pour définir ce qui est appelé un finaliseur. Les développeurs venant du monde C++ pensent de façon naturelle qu’il s’agit d’un nettoyage déterministe. Il aurait été préférable qu’une syntaxe différente soit adoptée et aiderai plus à faire comprendre qu’un environnement géré est très différent d’une architecture non gérée. |
Voici un exemple plus sophistiqué du modèle Dispose qui reprend tout ce qui a été dit précédemment.
|
#region using using System; using System.Security; using System.ComponentModel; using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; #endregion
public class
ComplexWindow :
IDisposable
private
MySafeHandleSubclass handle;
// pointeur vers
une ressource #region Constructeur
///
<summary>
component =
new
Component();
// implementation de
IDisposable
protected
virtual
void
Dispose(bool
disposing) // hold, and take yourself off the Finalization queue.
if
(disposing)
if (handle !=
null)
if (component !=
null)
//
libérez ici vos propres états (objets non
gérés) #endregion
#region
Finaliseur
#region
AdditionalCleanup * Cette méthode ne doit pas faire d'allocation ou poser de verrous * sauf nécessité impérieuse en raison d'un problème de sécurité par exemple * puisqu'elle sera appelée durant la finalisation. * Elle est également sujette à toutes les restrictions relatives aux finaliseurs * */
}
#region ShowWindow // chaque fois que vous accédez à cette classe, vérifiez si son état disposed // est à true. dans ce cas levez une exception.
public
void
ShowWindow()
//
sinon fait son boulot
[DllImport("user32.dll",
SetLastError = true,CharSet
= CharSet.Auto,
BestFitMapping =
false)]
#region
SafeMyResourceHandle (internal)
//
appelé par P/Invoke lorsque la méthode
externe retournera // Inutile de fournir un finaliseur. SafeHandle est un finaliseur critique et appelera pour // vous ReleaseHandle
public
override
bool
IsInvalid get { return handle == IntPtr.Zero; } }
override
protected
bool
ReleaseHandle() // Cette méthode est une zone d'exécution limitée (CER) // Elle ne doit donc pas faire d'allocation return DeleteObject(href); }
[DllImport("gdi32.dll",
SuppressUnmanagedCodeSecurity)]
[DllImport("kernel32")] } #endregion }
// Classe dérivée
public class
MyComplexWindow : ComplexWindow private Component myComponent; // encore une autre ressource private bool disposed = false;
public
MyComplexWindow() myComponent = new Component(); }
#region Dispose
protected
override
void
Dispose(bool
disposing)
if
(!this.disposed)
// code exécuté
une seule fois
if
(disposing) // appel déterministe
if (myComponent !=
null) myComponent.Dispose(); } this.disposed = true; }
//
Effectue les autres nettoyages comme par
exemple
} base.Dispose(disposing); } #endregion } |
|
Brian Grunkemeyer : La classe dérivée MyComplexWindow n’a pas strictement besoin d’ajouter son propre indicateur disposed. Elle pourrait par exemple se contenter de vérifier si myComponent n’est pas null dans Dispose(bool) et dans chaque méthode utilisant ce type. Dispose(bool) doit encore chaîner avec sa classe de base dans tous les cas toutefois. L’inconvénient de cette approche est qu’elle devient plus compliquée à maintenir si vous ajoutez d’autres champs dans votre type. |
|
Note du traducteur : La classe SafeHandle est détaillée dans MSDN ici : http://msdn2.microsoft.com/fr-fr/library/system.runtime.interopservices.safehandle.aspx |
L’utilisation de classes héritant de SafeHandle permet d’envelopper un handle dans une ressource gérée. La classe fournit des moyens de protection contre les attaques par recyclage, un mécanisme de finalisation critique et un support spécial pour le marshalling géré/non géré. SafeHandle est une classe abstraite dont on doit hériter. Des exemples d’implémentation sont fournis par .NET comme SafeHandleZeroOrMinusOneIsInvalid.
Si votre ressource est très légère, par exemple un petit buffer non géré et n’est pas une cible pour une attaque par recyclage ou si votre scénario est très sensible aux performances, il est possible de ne pas utiliser SafeHandle. Cette classe à beaucoup d’avantages, comme par exemple la réduction du graphe de promotion du à la finalisation, protection contre les attaques par recyclage, garantie d’aucune fuite en cas de déchargement rude du domaine d’application, mais peut être relativement lourde dans certains cas.
|
Brian Grunkemeyer : Nous avons implémenté dans .NET un certain nombre de SafeHandle pour des ressources assez fréquentes comme SafeFileHandle et SafeWaitHandle dans l’espace de nom Microsoft.Win32.SafeHandles. Pour une discussion plus approfondie lisez cet article : http://blogs.msdn.com/bclteam/archive/2005/03/16/396900.aspx |
HandleCollector suit les handles non gérés de façon à déclencher une collection en réponse à la réalisation de certaines consignes spécifiées durant sa construction. Lorsque vous allouez une ressource gérée par un collecteur, appelez simplement Add(). Lorsqu’elle est libérée, appelez Remove(). Lorsque le nombre de handle dépasse un certain seuil, une collection est alors déclenchée.
|
Jeffrey Richter : En interne, HandleCollector et AddMemoryPresure appellent GC.Collect. Ce que ce guide de conception essaye de dire est qu’il y a des moments plus appropriés que d’autres pour appeler GC.Collect, mais que d’une façon générale on doit essayer d’éviter le plus possible. On doit avoir une vraie raison. HandleCollector et AddMemoryPresure existent pour de vraies raisons : il vaut mieux lancer une collection et altérer les performances que de voir l’application mal fonctionner en raison d’un manque de ressource non gérées. |
Cet exemple montre comment les handles GDI peuvent être limités par un seuil entre 10 et 50. La frontière basse est une suggestion pour commencer à songer à une collection tandis que la limite haute implique obligatoirement une collection :
|
HandleCollector collector = new HandleCollector("GdiHandles", 10, 50);
IntPtr CreateSolidBrush() collector.Add();
try collector.Remove();
throw;
void DeleteBrush(IntPtr handle) DeleteObjectImpl(handle); collector.Remove(); } |
GC.AddMemoryPressure indique au ramasse-miettes que le coût d’un objet géré est plus élevé qu’il n’apparaît sur la seule fois de ses ressources non gérées. On passe à cette fonction la taille des ressources non gérées, le ramasse-miettes adaptera sa stratégie de collection pour augmenter la pression sur cet objet.
GC.RemoveMemoryPressure réalise l’inverse une fois que la ressource a été libérée. L’exemple qui suit démontre ce mécanisme lors de l’allocation/désallocation d’un Bitmap:
|
public class
Bitmap : IDisposable private long bmpSize;
public
Bitmap(string
path) GC.AddMemoryPressure(bmpSize);
//
travail d'allocation
public
void
Dispose()
protected
void
Dispose(bool
disposing)
~Bitmap() |
Si vous allouez un grand nombre de petits octets, il est toutefois plus efficace d’ajouter et enlever la pression sur des blocs plus larges. Par exemple des méga-octets ou des centaines de kilo-octets à la fois. Vous pourriez souhaiter implémenter un gestionnaire de pression personnalisé pour prendre en charge ce scénario du genre de l’exemple de code qui suit. Le BitmapPressureManager ajoute ou supprime une pression par blocs de 500 ko de mémoire. La classe Bitmap précédente a été modifiée pour appeler BitmapPressureManager à la place.
|
public class
Bitmap : IDisposable private long bmpSize; public Bitmap(string path) {
bmpSize =
new
FileInfo(path).Length; public void Dispose() {
Dispose(true); protected void Dispose(bool disposing) {
//
travail de netoyage
~Bitmap()
internal static
class
BitmapPressureManager private const long threshold = 524288; // ajoute une pression par blocs de 500 ko private static long pressure; private static long committedPressure; private static readonly object sync = new object(); internal static void AddMemoryPressure(long amount) {
Interlocked.Add(ref
pressure, amount); internal static void RemoveMemoryPressure(long amount) {
AddMemoryPressure(-amount); private static void PressureCheck(){ if (Math.Abs(pressure - committedPressure) >= threshold){
lock
(sync) {
if (diff <
0)
committedPressure +=
diff; |
Auteur : Joe Duffy