Implémenter une méthode Dispose

La méthode Dispose est principalement implémentée pour libérer des ressources non managées. Lorsque vous travaillez avec des membres d’instance qui sont des implémentations IDisposable, il est courant d’effectuer des appels Dispose en cascade. Il existe d’autres raisons pour implémenter Dispose, par exemple pour libérer de la mémoire allouée, supprimer un élément qui a été ajouté à une collection, ou signaler la libération d’un verrou acquis.

Le récupérateur de mémoire .NET n’alloue pas de mémoire non managée, et n’en libère pas non plus. Le modèle pour supprimer un objet, dénommé modèle Dispose, impose un ordre sur la durée de vie d’un objet. Le modèle Dispose est utilisé pour les objets qui implémentent l’interface IDisposable. Ce modèle est courant lors de l’interaction avec des handles de fichiers et de canaux, des handles de Registre, des handles d’attente ou des pointeurs vers des blocs de mémoire non managée, car le récupérateur de mémoire ne peut pas récupérer des objets non managés.

Pour que les ressources soient toujours assurées d’être correctement nettoyées, une méthode Dispose doit être idempotent, et ainsi pouvoir être appelée à plusieurs reprises sans lever d’exception. En outre, les appels ultérieurs de Dispose ne doivent rien faire.

L’exemple de code fourni pour la méthode GC.KeepAlive montre comment le nettoyage de la mémoire peut entraîner l’exécution d’un finaliseur pendant qu’une référence non managée à l’objet ou à ses membres est toujours en cours d’utilisation. Il peut être préférable d’utiliser GC.KeepAlive pour rendre l’objet inéligible pour le nettoyage de la mémoire du début de la routine actuelle jusqu’au point où cette méthode est appelée.

Conseil

En ce qui concerne l’injection de dépendances, lors de l’inscription de services dans un IServiceCollection, la durée de vie du service est gérée implicitement en votre nom. Le IServiceProvider et le IHost correspondant orchestrent le nettoyage des ressources. Plus précisément, les implémentations de IDisposable et IAsyncDisposable sont correctement supprimées à la fin de leur durée de vie spécifiée.

Pour plus d’informations, consultez Injection de dépendances dans .NET.

Handles sécurisés

L’écriture de code pour le finaliseur d’un objet est une tâche complexe qui peut provoquer des problèmes si elle n’est pas effectuée correctement. Par conséquent, nous vous recommandons de construire des objets System.Runtime.InteropServices.SafeHandle au lieu d'implémenter un finaliseur.

Un System.Runtime.InteropServices.SafeHandle est un type managé abstrait qui encapsule un System.IntPtr qui identifie une ressource non managée. Sur Windows, il peut identifier un handle et, sur Unix, un descripteur de fichier. SafeHandle fournit toute la logique nécessaire pour s’assurer que cette ressource est publiée une seule fois, soit lorsque le SafeHandle est supprimé, soit lorsque toutes les références au SafeHandle ont été supprimées et que l’instance SafeHandle est finalisée.

System.Runtime.InteropServices.SafeHandle est une classe de base abstraite. Les classes dérivées fournissent des instances spécifiques pour différents types de handles. Ces classes dérivées valident quelles valeurs pour System.IntPtr sont considérées comme non valides et comment libérer le handle. Par exemple, SafeFileHandle dérive de SafeHandle pour encapsuler IntPtrs qui identifie les handles/descripteurs de fichier ouverts, et substitue sa méthode SafeHandle.ReleaseHandle() pour les fermer (via la fonction close sur Unix ou la fonction CloseHandle sur Windows). La plupart des API dans les bibliothèques .NET qui créent une ressource non managée l’encapsulent dans un SafeHandle et vous retournent ce SafeHandle en fonction des besoins, plutôt que de remettre le pointeur brut. Dans les situations où vous interagissez avec un composant non managé et obtenez un IntPtr pour une ressource non managée, vous pouvez créer votre propre type SafeHandle pour l’encapsuler. Par conséquent, peu de types non-SafeHandle doivent implémenter des finaliseurs. La plupart des implémentations de modèle Dispose finissent uniquement par encapsuler d’autres ressources managées, dont certaines peuvent être des objets SafeHandle.

Les classes dérivées suivantes de l’espace de noms Microsoft.Win32.SafeHandles fournissent des handles sécurisés.

Classe Ressources qu’elle contient
SafeFileHandle
SafeMemoryMappedFileHandle
SafePipeHandle
Fichiers, fichiers mappés en mémoire et canaux
SafeMemoryMappedViewHandle Vues de la mémoire
SafeNCryptKeyHandle
SafeNCryptProviderHandle
SafeNCryptSecretHandle
Constructions de chiffrement
SafeRegistryHandle les clés de Registre
SafeWaitHandle Handles d'attente

Dispose() et Dispose(bool)

L'interface IDisposable requiert l'implémentation d'une méthode unique sans paramètre, Dispose. En outre, toute classe non scellés doit avoir une méthode de surcharge Dispose(bool).

Les signatures de méthode sont les suivantes :

  • public non virtuelle (NotOverridable en Visual Basic) (implémentation IDisposable.Dispose).
  • protected virtual (Overridable en Visual Basic) Dispose(bool).

Méthode Dispose()

Étant donné que la méthode Disposepublic, non virtuelle (NotOverridable en Visual Basic) et sans paramètre est appelée lorsqu’elle n’est plus nécessaire (par un consommateur de type), son objectif est de libérer les ressources non managées, d’effectuer un nettoyage général et d’indiquer que le finaliseur, s’il en existe un, ne doit pas s’exécuter. La libération de la mémoire réelle associée à un objet managé est toujours du domaine du récupérateur de mémoire. De ce fait, son implémentation standard est la suivante :

public void Dispose()
{
    // Dispose of unmanaged resources.
    Dispose(true);
    // Suppress finalization.
    GC.SuppressFinalize(this);
}
Public Sub Dispose() _
    Implements IDisposable.Dispose
    ' Dispose of unmanaged resources.
    Dispose(True)
    ' Suppress finalization.
    GC.SuppressFinalize(Me)
End Sub

La méthode Dispose effectue le nettoyage de tous les objets, le récupérateur de mémoire n'a plus donc besoin d'appeler la remplacement de Object.Finalize des objets. Par conséquent, l'appel à la méthode SuppressFinalize empêche le récupérateur de mémoire d'exécuter le finaliseur. Si le type n'a pas de finaliseur, l'appel à GC.SuppressFinalize n'a aucun effet. Le nettoyage réel est effectué par la surcharge de méthode Dispose(bool).

Surcharge de méthode Dispose(bool)

Dans la surcharge, le paramètre disposing est un Boolean qui indique si l’appel de la méthode provient d’une méthode Dispose (sa valeur est true) ou d’un finaliseur (sa valeur est false).

protected virtual void Dispose(bool disposing)
{
    if (_disposed)
    {
        return;
    }

    if (disposing)
    {
        // TODO: dispose managed state (managed objects).
    }

    // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
    // TODO: set large fields to null.

    _disposed = true;
}
Protected Overridable Sub Dispose(disposing As Boolean)
     If disposed Then Exit Sub	

     ' A block that frees unmanaged resources.
     
     If disposing Then
         ' Deterministic call…
         ' A conditional block that frees managed resources.    	
     End If
     
     disposed = True
End Sub

Important

Le paramètre disposing doit être false en cas d’appel à partir d’un finaliseur et true en cas d’appel à partir de la méthode IDisposable.Dispose . En d’autres termes, il est true en cas d’appel déterministe et false en cas d’appel non déterministe.

Le corps de la méthode se compose de trois blocs de code :

  • Un bloc pour le retour conditionnel si l’objet est déjà supprimé.

  • Un bloc qui libère les ressources non managées. Ce bloc s'exécute indépendamment de la valeur du paramètre disposing.

  • Un bloc conditionnel qui libère les ressources managées. Ce bloc s'exécute si la valeur de disposing est true. Les ressources managées qu'il libère peuvent inclure :

    • Des objets managés qui implémentent IDisposable. Le bloc conditionnel peut être utilisé pour appeler leur implémentation de Dispose (suppression en cascade). Si vous avez utilisé un une classe dérivée de System.Runtime.InteropServices.SafeHandle pour encapsuler votre ressource non managée, vous devez appeler l’implémentation de SafeHandle.Dispose() ici.

    • Objets managés qui consomment de grandes quantités de mémoire ou consomment des ressources rares. Affectez des références d’objets managés volumineux à null pour les rendre plus susceptibles d’être inaccessibles. Cela les libère plus rapidement que s’ils étaient récupérés de manière non déterministe.

Si l’appel de la méthode vient d’un finaliseur, seul le code qui libère les ressources non managées doit s’exécuter. L’implémenteur est chargé de s’assurer que le faux chemin n’interagit pas avec des objets managés susceptibles d’avoir été supprimés. Cela est important, car l’ordre dans lequel le récupérateur de mémoire élimine les objets managés pendant la finalisation n’est pas déterministe.

Appels de suppression en cascade

Si votre classe possède un champ ou une propriété et que son type implémente IDisposable, la classe contenante proprement dite doit également implémenter IDisposable. Une classe qui instancie une implémentation IDisposable et la stocke en tant que membre d’instance est également responsable de son nettoyage. Cela permet de garantir que les types supprimables référencés ont la possibilité d’effectuer un nettoyage déterministe par le biais de la méthode Dispose. Dans l’exemple suivant, la classe est sealed (ou NotInheritable en Visual Basic).

using System;

public sealed class Foo : IDisposable
{
    private readonly IDisposable _bar;

    public Foo()
    {
        _bar = new Bar();
    }

    public void Dispose() => _bar.Dispose();
}
Public NotInheritable Class Foo
    Implements IDisposable

    Private ReadOnly _bar As IDisposable

    Public Sub New()
        _bar = New Bar()
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        _bar.Dispose()
    End Sub
End Class

Conseil

  • Si votre classe a un champ ou une propriété IDisposable mais qu’elle n’en est pas propriétaire, ce qui signifie que la classe ne crée pas l’objet, la classe n’a pas besoin d’implémenter IDisposable.
  • Il existe des cas où vous souhaiterez peut-être effectuer la vérification de null dans un finaliseur (ce qui inclut la méthode Dispose(false) appelée par un finaliseur). L’une des principales raisons est si vous ne savez pas si l’instance a été entièrement initialisée (par exemple, une exception peut être levée dans un constructeur).

Implémenter le modèle Dispose

Toutes les classes non sealed (ou les classes Visual Basic non modifiées en tant que NotInheritable) doivent être considérées comme des classes de base potentielles, car elles peuvent être héritées. Si vous implémentez le modèle Dispose pour une classe de base potentielle, vous devez spécifier ce qui suit :

  • Une implémentation de Dispose qui appelle la méthode Dispose(bool).
  • Une méthode Dispose(bool) qui effectue le nettoyage réel.
  • Une classe dérivée de SafeHandle qui encapsule votre ressource managée (recommandée) ou une substitution de la méthode Object.Finalize. La classe SafeHandle fournit un finaliseur, de sorte que vous n’avez pas à en écrire un vous-même.

Important

Il est possible pour une classe de base de référencer uniquement des objets managés et d’implémenter le modèle Dispose. Dans ce cas, un finaliseur n’est pas nécessaire. Un finaliseur n’est requis que si vous référencez directement des ressources non managées.

Voici un exemple général d’implémentation du modèle de suppression d’une classe de base qui utilise un levier sécurisé.

using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;

public class BaseClassWithSafeHandle : IDisposable
{
    // To detect redundant calls
    private bool _disposedValue;

    // Instantiate a SafeHandle instance.
    private SafeHandle? _safeHandle = new SafeFileHandle(IntPtr.Zero, true);

    // Public implementation of Dispose pattern callable by consumers.
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    // Protected implementation of Dispose pattern.
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                _safeHandle?.Dispose();
                _safeHandle = null;
            }

            _disposedValue = true;
        }
    }
}
Imports Microsoft.Win32.SafeHandles
Imports System.Runtime.InteropServices

Public Class BaseClassWithSafeHandle
    Implements IDisposable

    ' To detect redundant calls
    Private _disposedValue As Boolean

    ' Instantiate a SafeHandle instance.
    Private _safeHandle As SafeHandle = New SafeFileHandle(IntPtr.Zero, True)

    ' Public implementation of Dispose pattern callable by consumers.
    Public Sub Dispose() _
               Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    ' Protected implementation of Dispose pattern.
    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        If Not _disposedValue Then

            If disposing Then
                _safeHandle?.Dispose()
                _safeHandle = Nothing
            End If

            _disposedValue = True
        End If
    End Sub
End Class

Notes

L'exemple précédent utilise un objet SafeFileHandle pour illustrer le modèle, mais il est possible d'utiliser à la place n'importe quel objet dérivé de SafeHandle. Notez que l'exemple n'instancie pas correctement son objet SafeFileHandle.

Voici le modèle général d’implémentation du modèle de suppression d’une classe de base qui remplace Object.Finalize.

using System;

public class BaseClassWithFinalizer : IDisposable
{
    // To detect redundant calls
    private bool _disposedValue;

    ~BaseClassWithFinalizer() => Dispose(false);

    // Public implementation of Dispose pattern callable by consumers.
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    // Protected implementation of Dispose pattern.
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects)
            }

            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
            // TODO: set large fields to null
            _disposedValue = true;
        }
    }
}
Public Class BaseClassWithFinalizer
    Implements IDisposable

    ' To detect redundant calls
    Private _disposedValue As Boolean

    Protected Overrides Sub Finalize()
        Dispose(False)
    End Sub

    ' Public implementation of Dispose pattern callable by consumers.
    Public Sub Dispose() _
               Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    ' Protected implementation of Dispose pattern.
    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        If Not _disposedValue Then

            If disposing Then
                ' TODO: dispose managed state (managed objects)
            End If

            ' TODO free unmanaged resources (unmanaged objects) And override finalizer
            ' TODO: set large fields to null
            _disposedValue = True
        End If
    End Sub
End Class

Conseil

En C#, vous implémentez une finalisation en fournissant un finaliseur, et non en substituant Object.Finalize. En Visual Basic, vous créez un finaliseur avec Protected Overrides Sub Finalize().

Implémenter le modèle Dispose pour une classe dérivée

Une classe dérivée d'une classe qui implémente l'interface IDisposable ne doit pas implémenter IDisposable, car l'implémentation de la classe de base de IDisposable.Dispose est héritée par les classes dérivées. Au lieu de cela, pour nettoyer une classe dérivée, vous fournissez les éléments suivants :

  • Une méthode protected override void Dispose(bool) qui substitue la méthode de la classe de base et effectue le nettoyage réel de la classe dérivée. Cette méthode doit également appeler la méthode base.Dispose(bool) (MyBase.Dispose(bool) en Visual Basic) en lui passant l’état de suppression (paramètre bool disposing) comme argument.
  • Une classe dérivée de SafeHandle qui encapsule votre ressource managée (recommandée) ou une substitution de la méthode Object.Finalize. La classe SafeHandle fournit un finaliseur qui vous permet de ne pas avoir à en coder un. Si vous fournissez un finaliseur, il doit appeler la surcharge Dispose(bool) avec un argument false.

Voici un exemple du modèle général d’implémentation du modèle Dispose pour une classe dérivée qui utilise un handle sécurisé :

using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;

public class DerivedClassWithSafeHandle : BaseClassWithSafeHandle
{
    // To detect redundant calls
    private bool _disposedValue;

    // Instantiate a SafeHandle instance.
    private SafeHandle? _safeHandle = new SafeFileHandle(IntPtr.Zero, true);

    // Protected implementation of Dispose pattern.
    protected override void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                _safeHandle?.Dispose();
                _safeHandle = null;
            }

            _disposedValue = true;
        }

        // Call base class implementation.
        base.Dispose(disposing);
    }
}
Imports Microsoft.Win32.SafeHandles
Imports System.Runtime.InteropServices

Public Class DerivedClassWithSafeHandle
    Inherits BaseClassWithSafeHandle

    ' To detect redundant calls
    Private _disposedValue As Boolean

    ' Instantiate a SafeHandle instance.
    Private _safeHandle As SafeHandle = New SafeFileHandle(IntPtr.Zero, True)

    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        If Not _disposedValue Then

            If disposing Then
                _safeHandle?.Dispose()
                _safeHandle = Nothing
            End If

            _disposedValue = True
        End If

        ' Call base class implementation.
        MyBase.Dispose(disposing)
    End Sub
End Class

Notes

L'exemple précédent utilise un objet SafeFileHandle pour illustrer le modèle, mais il est possible d'utiliser à la place n'importe quel objet dérivé de SafeHandle. Notez que l'exemple n'instancie pas correctement son objet SafeFileHandle.

Voici le modèle général d'implémentation du modèle de suppression d'une classe dérivée qui remplace Object.Finalize :

public class DerivedClassWithFinalizer : BaseClassWithFinalizer
{
    // To detect redundant calls
    private bool _disposedValue;

    ~DerivedClassWithFinalizer() => Dispose(false);

    // Protected implementation of Dispose pattern.
    protected override void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects).
            }

            // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
            // TODO: set large fields to null.
            _disposedValue = true;
        }

        // Call the base class implementation.
        base.Dispose(disposing);
    }
}
Public Class DerivedClassWithFinalizer
    Inherits BaseClassWithFinalizer

    ' To detect redundant calls
    Private _disposedValue As Boolean

    Protected Overrides Sub Finalize()
        Dispose(False)
    End Sub

    ' Protected implementation of Dispose pattern.
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        If Not _disposedValue Then

            If disposing Then
                ' TODO: dispose managed state (managed objects).
            End If

            ' TODO free unmanaged resources (unmanaged objects) And override a finalizer below.
            ' TODO: set large fields to null.
            _disposedValue = True
        End If

        ' Call the base class implementation.
        MyBase.Dispose(disposing)
    End Sub
End Class

Voir aussi