Esaminare i finalizzatori di classe

Completato

I finalizzatori (storicamente definiti distruttori) vengono usati per eseguire qualsiasi pulizia finale necessaria quando un'istanza di classe viene raccolta dal Garbage Collector (GC). Nella maggior parte dei casi, è possibile evitare di scrivere un finalizzatore usando le classi System.Runtime.InteropServices.SafeHandle o derivate per eseguire il wrapping di qualsiasi handle non gestito.

Alcuni aspetti da tenere presenti quando si usano i finalizzatori:

  • I finalizzatori non possono essere definiti nei tipi struct. Vengono usati solo con le classi.
  • Una classe può avere un solo finalizzatore.
  • I finalizzatori non possono essere ereditati o sovraccaricati.
  • Non è possibile chiamare i finalizzatori. Vengono richiamati automaticamente.
  • Un finalizzatore non accetta modificatori o ha parametri.

Sintassi di un finalizzatore

In C#, un finalizzatore viene definito usando una tilde (~) seguita dal nome della classe. Non accetta parametri e non può essere chiamato in modo esplicito.


class Car
{
    ~Car()  // finalizer
    {
        // cleanup statements...
    }
}

Un finalizzatore può essere implementato anche come definizione del corpo dell'espressione, come illustrato nell'esempio seguente.


public class Destroyer
{
   public override string ToString() => GetType().Name;

   ~Destroyer() => Console.WriteLine($"The {ToString()} finalizer is executing.");
}

Relazione tra Garbage Collection e finalizzatori

Il Garbage Collector in .NET gestisce automaticamente l'allocazione e il rilascio della memoria per gli oggetti gestiti. Quando non viene più fatto riferimento a un oggetto, il GC lo contrassegna per la raccolta e alla fine recupera la memoria. Se una classe dispone di un finalizzatore, il GC chiama il finalizzatore prima di recuperare la memoria dell'oggetto. Il finalizzatore consente all'oggetto di rilasciare qualsiasi risorsa non gestita che contiene.

Il processo di finalizzazione prevede in genere i passaggi seguenti:

  • Quando GC rileva che un oggetto con un finalizzatore non è più raggiungibile, sposta l'oggetto in una coda di finalizzazione.
  • Il thread finalizzatore esegue il metodo finalizzatore.
  • Dopo l'esecuzione del finalizzatore, l'oggetto viene spostato nella coda di freachable GC, in cui è idoneo per l'operazione di Garbage Collection nel ciclo GC successivo.

Esempio di finalizzatore

public class ResourceHolder
{
    // Unmanaged resource
    private IntPtr unmanagedResource;

    // Constructor
    public ResourceHolder()
    {
        // Allocate unmanaged resource
        unmanagedResource = /* allocate resource */;
    }

    // Finalizer
    ~ResourceHolder()
    {
        // Release unmanaged resource
        if (unmanagedResource != IntPtr.Zero)
        {
            // Free the resource
            /* free resource */
            unmanagedResource = IntPtr.Zero;
        }
    }
}

Implementare un modello dispose con un finalizzatore

Le interfacce come IDisposable e IAsyncDisposable vengono usate per rilasciare le risorse in modo deterministico. Il metodo Dispose viene chiamato in modo esplicito per rilasciare le risorse, mentre il finalizzatore funge da rete di sicurezza per garantire che le risorse vengano rilasciate anche se non viene chiamato Dispose.

Importante

La creazione e l'uso di interfacce esula dall'ambito di questo modulo. L'uso di IDisposable è descritto qui per fornire il contesto per il modello "dispose". Il training che fornisce un'introduzione alle interfacce è disponibile nella piattaforma Microsoft Learn.

Quando si usano finalizzatori, tenere presente quanto segue:

  • I finalizzatori non sono deterministici, ovvero non è possibile prevedere quando viene eseguito il finalizzatore. Dipende dalla pianificazione del GC. Questo comportamento può causare ritardi nel rilascio di risorse non gestite.
  • I finalizzatori possono influire sulle prestazioni perché gli oggetti con finalizzatori richiedono più tempo per essere raccolti.
  • È consigliabile implementare l'interfaccia IDisposable e il metodo Dispose per la pulizia deterministica delle risorse. Il metodo Dispose può essere chiamato in modo esplicito per rilasciare le risorse e il finalizzatore può essere usato come rete di sicurezza.

Ecco un esempio di classe che implementa l'interfaccia IDisposable e un finalizzatore:

public class ResourceHolder : IDisposable
{
    // Unmanaged resource
    private IntPtr unmanagedResource;
    private bool disposed = false;

    // Constructor
    public ResourceHolder()
    {
        // Allocate unmanaged resource
        unmanagedResource = /* allocate resource */;
    }

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

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Free other managed objects
            }

            // Free unmanaged resources
            if (unmanagedResource != IntPtr.Zero)
            {
                /* free resource */
                unmanagedResource = IntPtr.Zero;
            }

            disposed = true;
        }
    }

    // Finalizer
    ~ResourceHolder()
    {
        Dispose(false);
    }
}

In questo esempio:

  • Il metodo Dispose viene chiamato per rilasciare le risorse in modo deterministico.
  • Il finalizzatore chiama Dispose(false) per rilasciare risorse non gestite se non è stato chiamato Dispose.
  • GC.SuppressFinalize(this) viene usato per impedire l'esecuzione del finalizzatore se Dispose è già stato chiamato.