Le guide du Routard du modèle "Dispose"

  Auteur original : Joe Duffy (http://www.bluebytesoftware.com/blog/default.aspx)
  Traducteur: Frédéric De Lène Mirouze (amethyste16@hotmail.com)

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

Remerciements

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.

Introduction du traducteur

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.

A qui s’adresse cet article ?

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 :

http://www.dotnetguru.org/modules.php?op=modload&name=News&file=article&sid=687&mode=thread&order=0&thold=0

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 !

Dispose, Finalisation et gestion des ressources

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.

Aperçu

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.

 Le nettoyage implicite

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é.

  Le nettoyage explicite

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.

  1. La fin de vie d’une ressource (par exemple un handle Win32)
  2. La fin de vie de l’objet conteneur de la ressource (comme une instance de FileStream)

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.

Le modèle Dispose

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);
      GC.SuppressFinalize(this);

}

Ou bien :

public void Dispose(){

      GC.SuppressFinalize(this);
      Dispose(true);

}

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.
J’ai depuis changé d’avis. La raison est que Dispose() appelle Dispose(true) qui peut certes lever une exception, mais lorsque Finalize sera appelé plus tard, c’est Dispose(false) qui sera appelé. Il se peut – ou ne se peut pas- qu’un autre chemin de code soit emprunté et qu’alors l’exception ne soit pas levée.

 

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.

  • Ne jamais ré-implémenter l’interface IDisposable ou bien surcharger Dispose() ou Finalize() si un type de base de la hiérarchie les a déjà défini de façon conforme au modèle de conception Dispose.
    Vous devez juste surcharger Dispose(boolean) et y intégrer votre logique de nettoyage afin d’être certain que les appels vers les méthodes de la classe de base seront effectués. Ré implémenter Finalize se traduit en fait par un appel inutile à Dispose(bool).

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.

 

  • Ne jamais créer de variantes autres de Dispose que celles-ci :
    void Dispose()
    void Dispose(bool disposing)


     

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).

Exemples d’implémentation

Le modèle simplifié sans finaliseur (C#)

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;
      private bool disposed = false; // Détection d'appels redondants

      public SimpleCleanup()
      {
            this.handle = /* instanciation */;
      }

      protected virtual void Dispose(bool disposing)
      {
            if (!disposed)
            {

                  if (disposing)
                  {
                        if (handle != null)
                        {
                             handle.Dispose();
                        }
                  }

                  disposed = true;
            }
      }

      public void Dispose()
      {
            Dispose(true);
      }

}

Cas avec finaliseur (C#)

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
{
      // Liste de champs nécessitant un nettoyage

      private bool disposed = false; // pour détecter les appels redondants

      public ComplexCleanupBase()
      {
            // allocation des ressources
      }

protected void Dispose(bool disposing)
{
          if (!disposed)
          {
                  if (disposing)
                  {
                        // Logique de nettoyage non finalisable
                  }

                  // logique de nettoyage partagée

                  disposed = true;
            }
      }

      ~ComplexCleanupBase()
      {
            Dispose(false);
      }

      public void Dispose()
      {
            Dispose(true);
            GC.SuppressFinalize(this);
      }

}

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
{
      // Des ressources supplémentaires qui nécessitent un nettoyage

      private bool disposed = false; // détection des appels redondants

      public ComplexCleanupExtender() : base()
      {
            // allocation des ressources liées à la nouvelle classe
      }

      protected override void Dispose(bool disposing)
      {
            if (!disposed)
            {
                  if (disposing)
                  {
                        // Logique de nettoyage non finalisable
                  }

                  // 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.

Exemple avec finaliseur (C++)

public ref class ComplexCleanupBase
{

private:    bool disposed;

public:

    ComplexCleanupBase() : disposed(false)
    {
        // allocation des ressources
    }

    // implémentation implicite de IDisposable

    virtual ~ComplexCleanupBase()    {

        Console::WriteLine("Base::~dtor");
        if (!disposed)  {

            // logique non finalisable

            this->!ComplexCleanupBase();
            disposed = true;
        }

    }

    virtual !ComplexCleanupBase()   {
        Console::WriteLine("Base::!finalizer");

       if (!disposed)    {
            disposed = true;
        }
    }

};

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();
           disposed = true;
        }
    }

    virtual !ComplexCleanupExtender()   {

         Console::WriteLine("Extender::!finalizer");

        if (!disposed)   {
            // logique partagée
            disposed = true;
        }
    }
}
;

Le problème du versionnage

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.

  • Introduire une méthode publique Dispose() ou une méthode protected Dispose(bool) dans une classe de base, lèvera des alertes de compilation dans les sous classes qui auront elle-même implémentées ces méthodes. Le développeur devra donc soit masquer ces nouvelles méthodes à l’aide du modificateur new ou la surcharger afin de clarifier ses intentions.
    Les développeurs qui traitent les alertes comme des erreurs risquent de ne pas pouvoir compiler leur code.
  • Introduire la méthode virtuelle protected Dispose(bool) peut ne pas avoir l’effet attendu si les sous classes ont déjà implémentées Dispose(bool). La sous classe ne se chaînera en effet pas à la méthode homonyme de sa classe de base. On risque alors d’observer des pertes de ressources temporaires et même des finalisations qui ne se font pas si la sous classe appelle GC.SuppressFinalize. Si vous êtes l’auteur de la sous classe, il est évidemment facile de corriger le problème en ajoutant un base.Dispose(bool). Mais pour les types diffusés publiquement, vous ne pouvez contrôler l’usage fait par vos utilisateurs.
  • Si vous sous classez un type IDisposable qui ne suit pas de façon correcte le modèle précédemment présenté, d’autres complications peuvent se produire.
    La table qui suit résume les différentes possibilités pour permettre à la sous classe de s’abonner correctement à la chaîne de contrôle de la durée de vie des ressources.

    Ce tableau explique comment écrire la sous classe de sorte que celles qui les dériveront ultérieurement dispose d’un contrat clair et standard. Les cas considérés incluent ce que l’on doit faire si la classe de base implémente IDisposable publiquement, de façon privée ou pas du tout, que la classe de base dispose ou non d’une méthode Dispose(bool) et/ou si la classe de base a ou non une surcharge de Finalize.

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.

Dispose

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.

 Conception de classes disposables

  • Toujours implémenter IDisposable sur chaque type ayant un finaliseur. Cela donne à l’utilisateur un moyen d’effectuer un nettoyage déterministe de ces mêmes ressources dont est responsable le finaliseur. Vous pourriez également implémenter Dispose sur des types sans finaliseur, par exemple lorsque vous détruisez transitivement des objets ou en utilisant des objets qui gèrent déjà leurs ressources avec des finaliseurs.

 

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).

 

  • Toujours concevoir vos méthodes Dispose de sorte à pouvoir être appelées plusieurs fois. La méthode peut parfaitement choisir de ne rien faire après le premier appel. Mais elle ne doit pas générer d’exception.

 

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.

 

  • Toujours détruire transitivement les champs concernés de votre type à l’intérieur d’une  méthode Dispose. On doit appeler Dispose() sur chaque champ dont le cycle de vie dépend de l’objet.
    Par exemple, considérons un cas où notre objet détient un champ privé de type TextReader. Alors on doit appeler la méthode Dispose du TextReader depuis la méthode Dispose du type principal. Si ce code est implémenté dans Dispose(bool), alors l’appel TextReader.Dispose ne doit se faire que si l’argument booléen est égal à true : atteindre d’autres objets durant la finalisation n’est pas conseillé. De plus, si votre objet ne contrôle pas la durée de vie d’un autre objet IDisposable, alors il ne doit pas chercher à appeler sa méthode Dispose puisqu’une autre partie du code peut parfaitement compter sur le fait qu’il soit actif. Négliger cela peut provoquer des bugs très subtils.

 

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++.

 

  • Considérez qu’il est toujours possible qu’un type IDisposable soit mis à null avant d’être détruit. Par exemple :

 

public class CyclicClassA : IDisposable
{

    private TextReader myReader;
    private CyclicClassB cycle;

    public void Dispose()
    {

        if (myReader != null)
        {
            ((IDisposable)myReader).Dispose();
            myReader = null;
        }

        if (cycle != null)
        {
            CyclicClassB b = cycle;
            cycle = null;
            b.Dispose();
        }
    }
}

public class CyclicClassB : IDisposable
{
    private Bitmap bmp;
    private CyclicClassA cycle;

    public void Dispose()
    {
        if (bmp != null)
        {
            bmp.Dispose();
           bmp = null;
        }

        if (cycle != null)
        {
            CyclicClassA a = cycle;
            cycle = null;
            a.Dispose();
        }
    }
}

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.

  • Evitez de lever une exception depuis Dispose à l’exception de situations critiques où le processus qui exécute le code est corrompu. Les utilisateurs s’attendent à ce que Dispose ne lève jamais d’exception. Examinons l’exemple suivant :

void NaiveConsumer()
{

    TextReader tr = new StreamReader(File.OpenRead("foo.txt"));

    try
    {

        // on fait quelque chose ici

    }
    finally
    {
        tr.Dispose();
        // d'autres trucs par là
    }

}

 

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.

  • Toujours rendre l’objet inutilisable après avoir été détruit. Recréer un objet qui a déjà été détruit est souvent difficile, surtout lorsque des destructions transitives se sont produites. Dans de tels cas, invoquer des appels à des méthodes de cet objet doit lever l’exception ObjectDisposedException. S’il est possible de reconstruire un état, soyez alors certain d’avertir l’utilisateur qu’il doit potentiellement re-détruire plusieurs fois l’objet : la première fois, et à chaque fois après la reconstruction.

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
{
    private bool disposed = false; // true: l'état de l'objet est à détruit

    public void Dispose()
    {
        Dispose(true);
        this.disposed = true;
        GC.SuppressFinalize(this);
    }

    // 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()
    {
        if (disposed)
        {
            ReOpen();
        }

        // fait le boulot
    }

    // Note:
    // le code qui suit n'est pas thread safe.
    // il ne s'agit pas d'un concept, mais d'une démonstration

    public void ReOpen()
    {
        this.disposed = false;

        GC.ReRegisterForFinalization(this); 

        handle = //obtient un nouveau handle

        otherRes = new OtherResource();

    }

}

 

  • Toujours implémenter une méthode Close pour le nettoyage des classes pour lesquelles une telle terminologie est un standard, comme un fichier ou un socket. Dans ce cas, il est recommandé d’implémenter Close exactement de la même manière que Dispose. L’expérience montre en effet que les développeurs ne pensent pas à appeler les deux méthodes, mais l’une ou l’autre. Essayer de déléguer la responsabilité du nettoyage à Dispose, par exemple en ajoutant un appel à Close() depuis Dispose(). Documenter soigneusement les situations qui s’écartent de ce modèle.

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.

  • Annuler les objets gérés volumineux dans la méthode Dispose. C’est rarement nécessaire, mais peut être intéressant si un champ est coûteux à conserver en l’état car cet objet peut éventuellement rester en mémoire fort longtemps. Le simple fait d’appeler Dispose ne signifie pas que les références de l’objet soient libérées. Cela peut arriver par exemple dans le cas d’un conteneur (l’objet que l’on détruit) qui est référencé depuis une variable à longue durée de vie (comme une variable statique) et n’est pas explicitement annulée. L’annuler peut permettre d’abréger son existence en mémoire en le rendant éligible plus tôt lors d’une collection du ramasse-miettes. La définition d’un objet volumineux et coûteux est bien sûr subjective et doit s’appuyer sur des mesures de performance.
  • Eviter de créer des objets IDisposable de type valeur et ne jamais créer de valeur dont dépend la durée de vie d’un objet géré directement. A l’exception des situations pour lesquelles une valeur ne sera jamais copiée (c’est à dire est uniquement locale dans le corps d’une méthode) ou copiée de façon très contrôlée, il est difficile de prédire comment la destruction d’une valeur va interagir avec la sémantique de passage par copie du type valeur. Par exemple une fois qu’une nouvelle copie a été obtenue, une autre copie pourrait accéder aux champs déjà détruits par une ancienne copie.

Travailler avec des objets IDisposable

·        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.

Le block using en C# et VB, la sémantique C++

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()
{
    using (Resource r = new Resource())
    {
        // utilise la ressource
    }

}

Ce qui s’écrirait en C++ :

void UseDisposableObject()

{

    Resource r;

 

    // utilise la ressource

}

Le code IL est l’équivalent du code suivant :

void UseDisposableObject()
{
      Resource resource = new Resource();

      try
      {
          // utiliser les ressources
      }
      finally
      {
          if (resource != null)
          {
              ((IDisposable)resource).Dispose();
          }
      }

}

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())
using
(Resource r2 = new Resource())
{
    // utilise r1 et r2

}

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

 

Finalisation

Si vous implémentez une logique de finalisation, il est important de le faire correctement. Les finaliseurs sont notoirement diffic