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 Dispose
public
, 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
esttrue
. 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éthodeDispose(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éthodebase.Dispose(bool)
(MyBase.Dispose(bool)
en Visual Basic) en lui passant l’état de suppression (paramètrebool 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 argumentfalse
.
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