Implementare un metodo Dispose

Il metodo Dispose viene implementato principalmente per rilasciare risorse non gestite. Quando si lavora con i membri dell'istanza che sono implementazioni IDisposable, è comune eseguire chiamate Dispose a catena. Esistono altri motivi per l'implementazione di Dispose, ad esempio, per liberare memoria allocata, rimuovere un elemento aggiunto a una raccolta o segnalare il rilascio di un blocco acquisito.

Il Garbage Collector .NET non alloca o rilascia memoria non gestita. Il criterio per eliminare un oggetto, definito criterio Dispose, definisce un ordine in base alla durata di un oggetto. Il criterio Dispose viene usato per gli oggetti che implementano l'interfaccia IDisposable. Questo modello è comune quando si interagisce con handle di file e pipe, handle del Registro di sistema, handle di attesa o puntatori a blocchi di memoria non gestita, perché il Garbage Collector non è in grado di recuperare oggetti non gestiti.

Al fine di garantire la corretta pulitura delle risorse in ogni occasione, un metodo Dispose deve essere idempotente, in modo che si possa chiamare più volte senza che venga generata un'eccezione. Inoltre, le chiamate successive di Dispose non dovrebbero eseguire alcuna operazione.

L'esempio di codice fornito per il metodo GC.KeepAlive mostra in che modo un finalizzatore può causare l'esecuzione di un finalizzatore mentre un riferimento non gestito all'oggetto, o i relativi membri, è ancora in uso. Può essere utile usare GC.KeepAlive per rendere l'oggetto non idoneo per l'operazione di Garbage Collection dall'inizio della routine corrente al punto in cui viene chiamato questo metodo.

Suggerimento

Per quanto riguarda l'inserimento delle dipendenze, quando si registrano dei servizi in un oggetto IServiceCollection, la durata del servizio viene gestita in modo implicito per conto dell'utente. IServiceProvider e IHost corrispondente orchestrano la pulizia delle risorse. In particolare, le implementazioni di IDisposable e IAsyncDisposable vengono eliminate correttamente alla fine della durata specificata.

Per altre informazioni, vedere Inserimento delle dipendenze in .NET.

Handle sicuri

La scrittura di codice per il finalizzatore di un oggetto è un'attività complessa che può causare problemi se non eseguita correttamente. È pertanto consigliabile costruire oggetti System.Runtime.InteropServices.SafeHandle anziché implementare un finalizzatore.

System.Runtime.InteropServices.SafeHandle è un tipo gestito astratto che esegue il wrapping di un oggetto System.IntPtr che identifica una risorsa non gestita. In Windows potrebbe identificare un handle, e in Unix un descrittore di file. SafeHandle fornisce tutta la logica necessaria per garantire che questa risorsa venga rilasciata una volta e una sola volta, quando SafeHandle viene eliminato o quando tutti i riferimenti a SafeHandle sono stati eliminati e l'istanza SafeHandle viene finalizzata.

System.Runtime.InteropServices.SafeHandle è una classe di base astratta. Le classi derivate forniscono istanze specifiche per diversi tipi di handle. Queste classi derivate convalidano quali valori per System.IntPtr sono considerati non validi e come liberare effettivamente l'handle. Ad esempio, SafeFileHandle deriva da SafeHandle per eseguire il wrapping di IntPtrs che identifica handle/descrittori di file aperti ed esegue l'override del relativo metodo SafeHandle.ReleaseHandle() per chiuderlo (tramite la funzione close in Unix o la funzione CloseHandle in Windows). La maggior parte delle API nelle librerie .NET che creano una risorsa non gestita esegue il wrapping in un SafeHandle oggetto e lo restituisce SafeHandle in base alle esigenze, invece di restituire il puntatore non elaborato. Nelle situazioni in cui si interagisce con un componente non gestito e si ottiene un oggetto IntPtr per una risorsa non gestita, è possibile creare il proprio tipo SafeHandle per eseguirne il wrapping. Di conseguenza, alcuni tipi non SafeHandle devono implementare i finalizzatori. La maggior parte delle implementazioni di modelli eliminabili comporta solo il wrapping di altre risorse gestite, alcune delle quali possono essere oggetti SafeHandle.

Le seguenti classi derivate nello spazio dei nomi Microsoft.Win32.SafeHandles forniscono handle sicuri.

Classe Risorse che contiene
SafeFileHandle
SafeMemoryMappedFileHandle
SafePipeHandle
File, file mappati alla memoria e pipe
SafeMemoryMappedViewHandle Visualizzazioni di memoria
SafeNCryptKeyHandle
SafeNCryptProviderHandle
SafeNCryptSecretHandle
Costrutti di crittografia
SafeRegistryHandle Chiavi del Registro di sistema
SafeWaitHandle Handle di attesa

Dispose() e Dispose(bool)

L'interfaccia IDisposable richiede l'implementazione di un singolo metodo senza parametri, Dispose. Inoltre, qualsiasi classe non sealed deve avere un metodo di overload Dispose(bool).

Le firme del metodo sono:

  • public non virtuale (NotOverridable in Visual Basic) (implementazione IDisposable.Dispose).
  • protected virtual (Overridable in Visual Basic) Dispose(bool).

Metodo Dispose()

Poiché il metodo public, non virtuale (NotOverridable in Visual Basic), Dispose senza parametri viene chiamato quando non è più necessario (da un consumer del tipo), il suo scopo è liberare risorse non gestite, eseguire la pulizia generale e indicare che il finalizzatore, se presente, non deve essere eseguito. Liberare la memoria effettiva associata a un oggetto gestito è sempre il dominio del Garbage Collector. Per questo motivo il metodo ha un'implementazione standard:

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

Il metodo Dispose esegue la pulizia di tutti gli oggetti, quindi il Garbage Collector non deve più chiamare l'override Object.Finalize degli oggetti. Pertanto, la chiamata al metodo SuppressFinalize impedisce al Garbage Collector di eseguire il finalizzatore. Se il tipo non dispone di un finalizzatore, la chiamata a GC.SuppressFinalize non ha alcun effetto. La pulizia effettiva viene eseguita dall'overload del metodo Dispose(bool).

Overload del metodo Dispose(bool)

Nell’overload, il parametro disposing è un oggetto Boolean che indica se la chiamata al metodo proviene da un metodo Dispose (il valore è true) o da un finalizzatore (il valore è 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

Importante

Il parametro disposing deve essere false quando viene chiamato da un finalizzatore, e true quando viene chiamato dal metodo IDisposable.Dispose. In altre parole, è true quando viene chiamato in modo deterministico e false quando viene chiamato in modo non deterministico.

Il corpo del metodo è costituito da tre blocchi di codice:

  • Blocco per la restituzione condizionale se l'oggetto è già eliminato.

  • Un blocco che libera le risorse non gestite. Questo blocco viene eseguito indipendentemente dal valore del parametro disposing.

  • Un blocco condizionale che libera le risorse gestite. Il blocco è eseguito se il valore di disposing è true. Le risorse gestite liberate possono includere:

    • Oggetti gestiti che implementano IDisposable. Il blocco condizionale può essere usato per chiamare la relativa implementazione Dispose (eliminazione a catena). Se è stata usata una classe derivata di System.Runtime.InteropServices.SafeHandle per eseguire il wrapping della risorsa non gestita, è necessario chiamare l'implementazione SafeHandle.Dispose() qui.

    • Oggetti gestiti che usano grandi quantità di memoria o risorse insufficienti. Assegnare riferimenti a oggetti gestiti di grandi dimensioni a null per renderli più facilmente irraggiungibili. Queste versioni vengono rilasciate più velocemente rispetto a quelle recuperate in modo non deterministico.

Se la chiamata al metodo proviene da un finalizzatore, deve essere eseguito solo il codice che libera le risorse non gestite. L'implementatore è responsabile di garantire che il percorso false non interagisca con gli oggetti gestiti che potrebbero essere stati eliminati. Questo è importante perché l'ordine in cui il Garbage Collector elimina gli oggetti gestiti durante la finalizzazione non è deterministico.

Chiamate dispose a catena

Se la classe è proprietaria di un campo o di una proprietà e il relativo tipo implementa IDisposable, anche la stessa classe contenitore deve implementare IDisposable. Una classe che crea un'implementazione IDisposable e la archivia come membro dell'istanza è anche responsabile della pulizia. Ciò consente di garantire che ai tipi eliminabili a cui si fa riferimento sia data la possibilità di eseguire la pulizia in modo deterministico tramite il metodo Dispose. Nell'esempio seguente la classe è sealed (o NotInheritable in 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

Suggerimento

  • Se la classe ha un campo o una proprietà IDisposable ma non ne è proprietaria, ovvero la classe non crea l'oggetto, la classe non ha bisogno di implementare IDisposable.
  • Esistono casi in cui è possibile eseguire il controllo di null in un finalizzatore (che include il metodo Dispose(false) richiamato da un finalizzatore). Uno dei motivi principali è se non si è certi che l'istanza sia stata inizializzata completamente (ad esempio, un'eccezione potrebbe essere generata in un costruttore).

Implementare lo schema Dispose

Tutte le classi non sealed (o le classi Visual Basic non modificate come NotInheritable) devono essere considerate una potenziale classe di base, perché potrebbero essere ereditate. Se si implementa il modello dispose per qualsiasi classe base potenziale, è necessario specificare quanto segue:

  • Un'implementazione Dispose che chiami il metodo Dispose(bool).
  • Metodo Dispose(bool) che esegue la pulizia effettiva.
  • Una classe derivata da SafeHandle che esegua il wrapping della risorsa non gestita (consigliato) o un override al metodo Object.Finalize. La classe SafeHandle fornisce un finalizzatore, quindi non è necessario scriverne uno manualmente.

Importante

È possibile che una classe di base faccia riferimento solo a oggetti gestiti e implementi il modello dispose. In questi casi, un finalizzatore non è necessario. Un finalizzatore è necessario solo se si fa riferimento direttamente a risorse non gestite.

Di seguito è riportato un esempio generale di implementazione del modello dispose per una classe di base che usa un handle sicuro.

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

Nota

L'esempio precedente usa un oggetto SafeFileHandle per illustrato il criterio; sarebbe possibile usare invece qualsiasi oggetto derivato da SafeHandle. Si noti che l'esempio non crea correttamente un'istanza del relativo oggetto SafeFileHandle.

Di seguito è illustrato il modello generale per implementare il modello Dispose per una classe di base che esegue l'override di 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

Suggerimento

In C# si implementa una finalizzazione fornendo un finalizzatore, non eseguendo l'override di Object.Finalize. In Visual Basic si crea un finalizzatore con Protected Overrides Sub Finalize().

Implementare il modello dispose per una classe derivata

Una classe derivata da una classe che implementa l'interfaccia IDisposable non deve implementare IDisposable, poiché l'implementazione della classe di base di IDisposable.Dispose viene ereditata dalle classi derivate. Per pulire invece una classe derivata, fornire quanto segue:

  • Metodo protected override void Dispose(bool) che esegue l'override del metodo della classe base ed esegue la pulizia effettiva della classe derivata. Questo metodo deve anche chiamare il metodo base.Dispose(bool) (MyBase.Dispose(bool) in Visual Basic) passando lo stato di eliminazione (parametrobool disposing) come argomento.
  • Una classe derivata da SafeHandle che esegua il wrapping della risorsa non gestita (consigliato) o un override al metodo Object.Finalize. La classe SafeHandle fornisce un finalizzatore, evitando la necessità di codificarne uno. Se si specifica un finalizzatore, deve chiamare l'overload di Dispose(bool) con argomento false.

Ecco un esempio del modello generale per implementare il modello dispose per una classe derivata che usa un handle sicuro:

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

Nota

L'esempio precedente usa un oggetto SafeFileHandle per illustrato il criterio; sarebbe possibile usare invece qualsiasi oggetto derivato da SafeHandle. Si noti che l'esempio non crea correttamente un'istanza del relativo oggetto SafeFileHandle.

Di seguito è illustrato il modello generale per implementare il modello Dispose per una classe derivata che esegue l'override di 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

Vedi anche