Dispose, modèle

Remarque

Ce contenu est réimprimé avec l’autorisation de Pearson Education, Inc. à partir des Instructions de conception d’une infrastructure : conventions, idiomes et modèles des bibliothèques réutilisables .NET, 2ème édition. Cette édition a été publiée en 2008, et le livre a été entièrement révisé dans la troisième édition. Certaines informations de cette page peuvent être obsolètes.

Tous les programmes acquièrent une ou plusieurs ressources système, telles que la mémoire, les descripteurs système ou les connexions de base de données, au cours de leur exécution. Les développeurs doivent être prudents lors de l’utilisation de telles ressources système, car elles doivent être publiées une fois qu’elles ont été acquises et utilisées.

Le CLR prend en charge la gestion automatique de la mémoire. La mémoire managée (mémoire allouée à l’aide de l’opérateur newC# ) n’a pas besoin d’être explicitement libérée. Cela est libéré automatiquement par le récupérateur de mémoire (GC). Cela libère les développeurs de la tâche fastidieuse et difficile de libérer de la mémoire et a été l’une des raisons principales de la productivité sans précédent offerte par .NET Framework.

Malheureusement, la mémoire managée n’est qu’un des nombreux types de ressources système. Les ressources autres que la mémoire managée doivent toujours être publiées explicitement et sont appelées ressources non managées. Le récupérateur de mémoire n’a pas été spécifiquement conçu pour gérer ces ressources non managées, ce qui signifie que la responsabilité de la gestion des ressources non managées est entre les mains des développeurs.

Le CLR vous aide à libérer des ressources non managées. System.Object déclare une méthode virtuelle Finalize (également appelée finaliseur) qui est appelée par le récupérateur de mémoire avant que la mémoire de l’objet ne soit récupérée par le récupérateur de mémoire et peut être remplacée pour libérer des ressources non managées. Les types qui remplacent le finaliseur sont appelés types finalisables.

Bien que les finaliseurs soient efficaces dans certains scénarios de nettoyage, ils présentent deux inconvénients importants :

  • Le finaliseur est appelé lorsque le récupérateur de mémoire détecte qu’un objet est éligible à la collection. Cela se produit à une période indéterminée après que la ressource ne soit plus nécessaire. Le délai entre le moment où le développeur peut ou souhaite libérer la ressource et le moment où la ressource est réellement publiée par le finaliseur peut être inacceptable dans les programmes qui acquièrent de nombreuses ressources rares (ressources qui peuvent être facilement épuisées) ou dans les cas où les ressources sont coûteuses à conserver (par exemple, les mémoires tampons non managées volumineuses).

  • Lorsque le CLR doit appeler un finaliseur, il doit reporter la collecte de la mémoire de l’objet jusqu’à la prochaine série de nettoyage de la mémoire (les finaliseurs s’exécutent entre les collections). Cela signifie que la mémoire de l’objet (et tous les objets auxquels il fait référence) ne sera pas libérée pendant une période plus longue.

Par conséquent, s’appuyer exclusivement sur des finaliseurs peut ne pas être approprié dans de nombreux scénarios lorsqu’il est important de récupérer des ressources non managées le plus rapidement possible, quand il s’agit de ressources rares ou dans des scénarios très performants dans lesquels la surcharge supplémentaire du récupérateur de mémoire liée à la finalisation est inacceptable.

L’infrastructure fournit l’interface System.IDisposable qui doit être implémentée pour fournir au développeur un moyen manuel de libérer des ressources non managées dès qu’elles ne sont pas nécessaires. Cela fournit également la méthode GC.SuppressFinalize qui peut indiquer au récupérateur de mémoire qu’un objet a été supprimé manuellement et n’a plus besoin d’être finalisé, auquel cas la mémoire de l’objet peut être récupérée plus tôt. Les types qui implémentent l’interface IDisposable sont appelés types jetables.

Le modèle Dispose est destiné à normaliser l’utilisation et l’implémentation des finaliseurs et de l’interface IDisposable.

La motivation principale du modèle est de réduire la complexité de l’implémentation des méthodes Finalize et Dispose. La complexité provient du fait que les méthodes partagent certains chemins de code, mais pas tous (les différences sont décrites plus loin dans le chapitre). En outre, il existe des raisons historiques pour certains éléments du modèle liés à l’évolution de la prise en charge de la langue pour la gestion déterministe des ressources.

✓ IMPLÉMENTE le modèle Dispose de base sur les types contenant des instances de types jetables. Pour plus d’informations sur le modèle de base, consultez la section Modèle Dispose de base.

Si un type est responsable de la durée de vie d’autres objets jetables, les développeurs doivent également disposer d’un moyen de les éliminer. L’utilisation de la méthode du conteneur Dispose est un moyen pratique de rendre cela possible.

✓ IMPLÉMENTE le modèle Dispose de base et fournit un finaliseur sur les types contenant des ressources qui doivent être libérées explicitement et qui n’ont pas de finaliseurs.

Par exemple, le modèle doit être implémenté sur les types stockant des mémoires tampons non managées. La section Types finalisables décrit les instructions relatives à l’implémentation des finaliseurs.

✓ ENVISAGEZ d’implémenter le modèle Dispose de base sur les classes qui elles-mêmes ne contiennent pas de ressources non managées ou d’objets jetables, mais qui sont susceptibles d’avoir des sous-types qui le font.

La classe System.IO.Stream en est un excellent exemple. Bien qu’il s’agisse d’une classe de base abstraite qui ne contient aucune ressource, la plupart de ses sous-classes le font et pour cette raison, elle implémente ce modèle.

Modèle Dispose de base

L’implémentation de base du modèle implique l’implémentation de l’interface System.IDisposable et la déclaration de la méthode Dispose(bool) qui implémente toute la logique de nettoyage des ressources à partager entre la méthode Dispose et le finaliseur facultatif.

L’exemple suivant montre une implémentation simple du modèle de base :

public class DisposableResourceHolder : IDisposable {

    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder() {
        this.resource = ... // allocates the resource
    }

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

    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            if (resource!= null) resource.Dispose();
        }
    }
}

Le paramètre booléen disposing indique si la méthode a été appelée à partir de l’implémentation de IDisposable.Dispose ou du finaliseur. L’implémentation Dispose(bool) doit vérifier le paramètre avant d’accéder à d’autres objets de référence (par exemple, le champ de ressource dans l’exemple précédent). Ces objets ne doivent être accessibles que lorsque la méthode est appelée à partir de l’implémentation de IDisposable.Dispose (lorsque le paramètre disposing est égal à true). Si la méthode est appelée à partir du finaliseur (disposing est false), les autres objets ne doivent pas être accessibles. La raison en est que les objets sont finalisés dans un ordre imprévisible et qu’ils, ou l’une de leurs dépendances, ont peut-être déjà été finalisés.

En outre, cette section s’applique aux classes avec une base qui n’implémente pas déjà le modèle Dispose . Si vous héritez d’une classe qui implémente déjà le modèle, remplacez simplement la méthode Dispose(bool) pour fournir une logique de nettoyage de ressources supplémentaire.

✓ DÉCLARE une méthode protected virtual void Dispose(bool disposing) pour centraliser toute la logique liée à la libération des ressources non managées.

Tout le nettoyage des ressources doit se produire dans cette méthode. La méthode est appelée à partir du finaliseur et de la méthode IDisposable.Dispose. Le paramètre sera false s’il est appelé à partir d’un finaliseur. Il doit être utilisé pour s’assurer que tout code en cours d’exécution pendant la finalisation n’accède pas à d’autres objets finalisables. Les détails de l’implémentation des finaliseurs sont décrits dans la section suivante.

protected virtual void Dispose(bool disposing) {
    if (disposing) {
        if (resource!= null) resource.Dispose();
    }
}

✓ IMPLÉMENTE l’interface IDisposable en appelant Dispose(true) simplement suivi de GC.SuppressFinalize(this).

L’appel à SuppressFinalize ne doit se produire que si Dispose(true) s’exécute correctement.

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

X NE PAS rendre virtuelle la méthode Dispose sans paramètre.

La méthode Dispose(bool) est celle qui doit être remplacée par des sous-classes.

// bad design
public class DisposableResourceHolder : IDisposable {
    public virtual void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

// good design
public class DisposableResourceHolder : IDisposable {
    public void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

X NE PAS déclarer les surcharges de la méthode Dispose autre que Dispose() et Dispose(bool).

Dispose doit être considéré comme un mot réservé pour aider à codifier ce modèle et éviter la confusion entre les implémenteurs, les utilisateurs et les compilateurs. Certaines langues peuvent choisir d’implémenter automatiquement ce modèle sur certains types.

✓ PERMET à la méthode Dispose(bool) d’être appelée plusieurs fois. La méthode peut choisir de ne rien faire après le premier appel.

public class DisposableResourceHolder : IDisposable {

    bool disposed = false;

    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

X ÉVITER de lever une exception depuis l’intérieur de Dispose(bool), sauf dans les situations critiques où le processus contenant a été endommagé (fuites, état partagé incohérent, etc.).

Les utilisateurs s’attendent à ce qu’un appel à Dispose ne déclenche pas d’exception.

Si Dispose peut déclencher une exception, la logique de nettoyage du bloc final ne s’exécute pas. Pour contourner ce problème, l’utilisateur doit encapsuler chaque appel à Dispose (dans le bloc final !) dans un bloc d’essai, ce qui conduit à des gestionnaires de nettoyage très complexes. Si vous exécutez une méthode Dispose(bool disposing), ne levez jamais d’exception si la suppression est false. Cela met fin au processus en cas d’exécution à l’intérieur d’un contexte de finaliseur.

✓ LANCE un ObjectDisposedException à partir d’un membre qui ne peut pas être utilisé une fois l’objet supprimé.

public class DisposableResourceHolder : IDisposable {
    bool disposed = false;
    SafeHandle resource; // handle to a resource

    public void DoSomething() {
        if (disposed) throw new ObjectDisposedException(...);
        // now call some native methods using the resource
        ...
    }
    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

✓ ENVISAGE de fournir la méthode Close(), en plus du Dispose(), si close est la terminologie standard dans la zone.

Dans ce cas, il est important que vous rendiez l’implémentation Close identique à Dispose et envisagiez d’implémenter la méthode IDisposable.Dispose explicitement.

public class Stream : IDisposable {
    IDisposable.Dispose() {
        Close();
    }
    public void Close() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Types finalisables

Les types finalisables sont des types qui étendent le modèle Dispose de base en remplaçant le finaliseur et en fournissant le chemin du code de finalisation dans la méthode Dispose(bool).

Il est notoire que les finaliseurs sont difficiles à implémenter correctement, principalement parce que vous ne pouvez pas faire certaines hypothèses (normalement valides) sur l’état du système pendant leur exécution. Les instructions suivantes doivent être prises en considération avec attention.

Notez que certaines des instructions s’appliquent non seulement à la méthode Finalize, mais à tout code appelé à partir d’un finaliseur. Dans le cas du modèle Dispose de base précédemment défini, cela signifie une logique qui s’exécute à l’intérieur de Dispose(bool disposing) lorsque le paramètre disposing est false.

Si la classe de base est déjà finalisable et implémente le modèle Dispose de base, vous ne devez pas remplacer Finalize à nouveau. Vous devez simplement remplacer la méthode Dispose(bool) pour fournir une logique de nettoyage de ressources supplémentaire.

Le code suivant montre un exemple de type finalisable :

public class ComplexResourceHolder : IDisposable {

    private IntPtr buffer; // unmanaged memory buffer
    private SafeHandle resource; // disposable handle to a resource

    public ComplexResourceHolder() {
        this.buffer = ... // allocates memory
        this.resource = ... // allocates the resource
    }

    protected virtual void Dispose(bool disposing) {
        ReleaseBuffer(buffer); // release unmanaged memory
        if (disposing) { // release other disposable objects
            if (resource!= null) resource.Dispose();
        }
    }

    ~ComplexResourceHolder() {
        Dispose(false);
    }

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

X ÉVITE de rendre les types finalisables.

Examinez attentivement tous les cas dans lesquels vous pensez qu’un finaliseur est nécessaire. Il existe un coût réel associé aux instances avec des finaliseurs, du point de vue des performances et de la complexité du code. Utilisez plutôt des wrappers de ressources tels que SafeHandle pour encapsuler des ressources non managées dans la mesure du possible, auquel cas un finaliseur devient inutile, car le wrapper est responsable de son propre nettoyage des ressources.

X NE PAS rendre les types de valeur finalisables.

Seuls les types de référence sont effectivement finalisés par le CLR, de sorte que toute tentative de placer un finaliseur sur un type valeur sera ignorée. Les compilateurs de C# et C++ appliquent cette règle.

✓ REND un type finalisable si le type est responsable de la libération d’une ressource non managée qui n’a pas son propre finaliseur.

Lors de l’implémentation du finaliseur, il vous suffit d’appeler Dispose(false) et de placer toutes les logiques de nettoyage des ressources à l’intérieur de la méthode Dispose(bool disposing).

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing) {
        ...
    }
}

✓ IMPLÉMENTEZ le modèle Dispose de base sur chaque type finalisable.

Cela permet aux utilisateurs du type d’effectuer explicitement un nettoyage déterministe des mêmes ressources dont le finaliseur est responsable.

X NE PAS accéder aux objets finalisables dans le chemin de code du finaliseur, car il existe un risque important qu’ils aient déjà été finalisés.

Par exemple, un objet finalisable A qui a une référence à un autre objet finalisable B ne peut pas utiliser B de manière fiable dans le finaliseur de A, ou vice versa. Les finaliseurs sont appelés dans un ordre aléatoire (à moins d’une garantie d’ordre faible pour la finalisation critique).

Sachez également que les objets stockés dans des variables statiques sont collectés à certains moments pendant le déchargement d’un domaine d’application ou lors de la sortie du processus. L’accès à une variable statique qui fait référence à un objet finalisable (ou l’appel d’une méthode statique qui peut utiliser des valeurs stockées dans des variables statiques) peut ne pas être sécurisé si Environment.HasShutdownStarted retourne true.

✓ REND votre méthode Finalize protégée.

Les développeurs de C#, C++ et VB.NET n’ont pas besoin de s’en soucier, car les compilateurs aident à appliquer cette directive.

X NE PAS laisser les exceptions s’échapper de la logique du finaliseur, à l’exception des défaillances critiques du système.

Si une exception est levée à partir d’un finaliseur, le CLR arrête l’ensemble du processus (à compter de .NET Framework version 2.0), ce qui empêche l’exécution d’autres finaliseurs et les ressources d’être libérées de manière contrôlée.

✓ ENVISAGE de créer et d’utiliser un objet critique finalisable (un type avec une hiérarchie de type qui contient CriticalFinalizerObject) pour les situations dans lesquelles un finaliseur doit absolument s’exécuter même en cas de déchargements forcés de domaine d’application et d’abandons de threads.

Portions © 2005, 2009 Microsoft Corporation. Tous droits réservés.

Réimprimé avec l’autorisation de Pearson Education, Inc. et extrait de Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition par Krzysztof Cwalina et Brad Abrams, publié le 22 octobre 2008 par Addison-Wesley Professional dans le cadre de la série sur le développement Microsoft Windows.

Voir aussi