Condividi tramite


Modello Dispose

Nota

Questo contenuto è ristampato con l'autorizzazione di Pearson Education, Inc. da Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition. Tale edizione è stata pubblicata nel 2008 e il libro è stato interamente revisionato nella terza edizione. Alcune delle informazioni contenute in questa pagina potrebbero non essere aggiornate.

Tutti i programmi acquisiscono una o più risorse di sistema, come memoria, handle di sistema o connessioni di database, durante la loro esecuzione. Gli sviluppatori devono fare attenzione nell’uso di tali risorse di sistema, poiché esse devono essere rilasciate dopo l'acquisizione e l'uso.

CLR fornisce assistenza per la gestione automatica della memoria. La memoria gestita (memoria allocata tramite l'operatore C# new) non ha bisogno di essere rilasciata esplicitamente. Viene rilasciata automaticamente dal Garbage Collector (GC). Grazie a ciò, gli sviluppatori non devono preoccuparsi del rilascio di memoria, attività complicata e tediosa; questa è una delle ragioni principali per cui .NET Framework offre una produttività senza precedenti.

Sfortunatamente, la memoria gestita è solo uno tra i molti tipi di risorse di sistema. Le risorse diverse dalla memoria gestita devono comunque essere rilasciate esplicitamente e sono definite risorse non gestite. Il GC è specificamente progettato per non gestire risorse non gestite, il che significa che gli sviluppatori sono responsabili della gestione di queste ultime.

CLR fornisce parziale assistenza nel rilascio di risorse non gestite. System.Object dichiara un metodo virtuale Finalize (detto anche finalizzatore), chiamato dal GC prima che la memoria dell'oggetto venga recuperata dal GC e possa essere sottoposta a override per rilasciare risorse non gestite. I tipi che eseguono l'override del finalizzatore vengono definiti tipi finalizzabili.

Anche se i finalizzatori sono efficaci in alcuni scenari di pulizia, presentano due svantaggi significativi:

  • Il finalizzatore viene chiamato quando il GC rileva che un oggetto è idoneo per la raccolta. Ciò si verifica in un periodo di tempo indeterminato dopo che la risorsa non è più necessaria. Il ritardo tra quando lo sviluppatore potrebbe o desidererebbe rilasciare una risorsa e l’effettivo rilascio della risorsa da parte dal finalizzatore potrebbe essere inaccettabile in programmi che acquisiscono un alto numero di risorse scarse (risorse facilmente esauribili) o in casi in cui le risorse sono dispendiose da mantenere in uso (ad esempio, grandi buffer di memoria non gestiti).

  • Quando il CLR necessita di un finalizzatore, deve rimandare la raccolta della memoria dell'oggetto fino al successivo round di Garbage Collection (i finalizzatori vengono eseguiti tra raccolte). Ciò significa che la memoria dell'oggetto (e tutti gli oggetti a cui fa riferimento) non verranno rilasciati per un periodo di tempo più lungo.

Pertanto, usare esclusivamente finalizzatori potrebbe non essere appropriato in molti scenari nei quali le risorse non gestite devono essere recuperate il più rapidamente possibile, quando si ha a che fare con risorse scarse, o in scenari con prestazioni elevate in cui il sovraccarico GC aggiuntivo della finalizzazione è inaccettabile.

Il Framework fornisce l'interfaccia System.IDisposable, che deve essere implementata per fornire allo sviluppatore un metodo manuale per rilasciare risorse non gestite appena diventano non necessarie. Fornisce inoltre il metodo GC.SuppressFinalize, che indica al GC se un oggetto è stato eliminato manualmente e non ha bisogno di essere finalizzato; in questo caso, la memoria dell'oggetto può essere recuperata in anticipo. I tipi che implementano l'interfaccia IDisposable vengono definiti tipi monouso.

Il modello Dispose è progettato per standardizzare l'utilizzo e l'implementazione dei finalizzatori e dell'interfaccia IDisposable.

La finalità principale del criterio è ridurre la complessità dell'implementazione dei metodi Finalize e Dispose. La complessità deriva dal fatto che i metodi hanno alcuni ma non tutti i percorsi di codice in comune (le differenze sono illustrate più avanti in questo capitolo). Esistono inoltre ragioni storiche per alcuni elementi del criterio, correlate all'evoluzione del supporto del linguaggio per la gestione risorse deterministica.

✓ DO implementare il criterio Dispose Basic su tipi contenenti istanze di tipi monouso. Per informazioni dettagliate sul modello di base, consultare la sezione Criterio Dispose Basic.

Se un tipo è responsabile della durata di altri oggetti monouso, anche gli sviluppatori necessitano di un metodo per eliminarli. Il metodo del contenitore Dispose è un modo pratico per rendere possibile questa operazione.

✓ IMPLEMENTARE il criterio Dispose Basic e fornire un finalizzatore per tipi contenenti risorse che necessitano di essere liberate esplicitamente e che non dispongono di finalizzatori.

Ad esempio, il criterio deve essere implementato su tipi che archiviano buffer di memoria non gestiti. La sezioneTipi finalizzabili illustra le linee guida relative all'implementazione dei finalizzatori.

✓ VALUTARE l'implementazione del criterio Dispose Basic su classi che non contengono risorse non gestite o oggetti eliminabili, ma che probabilmente possiedono sottotipi che lo fanno.

Un ottimo esempio di questa tipologia è la classe System.IO.Stream. Sebbene si tratti di una classe di base astratta che non contiene risorse, la maggior parte delle sue sottoclassi ne contiene; di conseguenza, la classe implementa questo criterio.

Criterio Dispose Basic

L'implementazione di base del modello prevede l'implementazione dell'interfaccia System.IDisposable e la dichiarazione del metodo Dispose(bool), il quale implementa la logica di pulizia di tutte le risorse perché sia condivisa tra il metodo Dispose e il finalizzatore opzionale.

Il seguente esempio illustra una semplice implementazione del criterio di 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();
        }
    }
}

Il parametro booleano disposing indica se il metodo è stato chiamato dall'implementazione IDisposable.Dispose o dal finalizzatore. L'implementazione Dispose(bool) deve controllare il parametro prima di accedere ad altri oggetti di riferimento (ad esempio, il campo della risorsa nell'esempio precedente). Accedere a tali oggetti solo quando il metodo viene chiamato dall'implementazione IDisposable.Dispose (quando il parametro disposing equivale a true). Se il metodo viene chiamato dal finalizzatore (disposing equivale a false), non è necessario accedere ad altri oggetti. Questo dipende dal fatto che gli oggetti vengono finalizzati in un ordine imprevedibile e, di conseguenza, essi o le relative dipendenze potrebbero essere già stati finalizzati.

Inoltre, questa sezione è rilevante per classi con una base che non implementa già il criterio Dispose. Se si eredita da una classe che implementa già il modello, è sufficiente eseguire l'override del metodo Dispose(bool) per fornire logica di pulizia delle risorse aggiuntiva.

✓ DICHIARARE un metodo protected virtual void Dispose(bool disposing) per centralizzare tutta la logica correlata al rilascio di risorse non gestite.

Tutte le operazioni di pulizia delle risorse devono essere eseguite con questo metodo. Il metodo viene chiamato sia dal finalizzatore che dal metodo IDisposable.Dispose. Il parametro indicherà false se chiamato dall'interno di un finalizzatore. Farne uso per assicurarsi che codici in esecuzione durante la finalizzazione non accedano ad altri oggetti finalizzabili. Dettagli sull'implementazione dei finalizzatori sono illustrati nella sezione successiva.

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

✓ IMPLEMENTARE l'interfaccia IDisposable semplicemente chiamando Dispose(true), seguito da GC.SuppressFinalize(this).

La chiamata a SuppressFinalize deve verificarsi solo se Dispose(true) viene eseguito correttamente.

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

NON rendere virtuale il metodo senza parametri Dispose.

Il metodo Dispose(bool) è quello che va sottoposto a override da sottoclassi.

// 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) { ... }
}

NON dichiarare overload del metodo Dispose, fatta eccezione per Dispose() e Dispose(bool).

Considerare Dispose come una parola riservata utile a codificare questo criterio e prevenire confusione tra implementatori, utenti e compilatori. Alcuni linguaggi potrebbero scegliere di implementare automaticamente questo criterio in determinati tipi.

✓ CONSENTIRE al metodo Dispose(bool) di essere chiamato più di una volta. Il metodo potrebbe scegliere di non eseguire alcuna operazione dopo la prima chiamata.

public class DisposableResourceHolder : IDisposable {

    bool disposed = false;

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

X EVITARE di generare un'eccezione dall'interno di Dispose(bool), eccetto in situazioni critiche in cui il processo contenitore è stato danneggiato (perdite, stato condiviso incoerente e così via).

Gli utenti si aspettano che una chiamata a Dispose non generi eccezioni.

Se Dispose potrebbe generare un'eccezione, ulteriore logica di pulizia del blocco finally non verrà eseguita. Per risolvere questo problema, l'utente deve eseguire il wrapping di ogni chiamata a Dispose (all'interno del blocco finally!) in un blocco try, comportando gestori di pulizia molto complessi. Se si esegue un metodo Dispose(bool disposing), non generare mai un'eccezione se l'eliminazione è false. In questo modo, il processo verrà terminato se eseguito all'interno di un contesto di finalizzatore.

✓ GENERARE un ObjectDisposedException da qualsiasi membro che non può essere utilizzato dopo l’eliminazione dell’oggetto.

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;
    }
}

✓ VALUTARE di fornire il metodo Close(), oltre al Dispose(), se close è la terminologia standard nell'area.

In questo caso, è importante rendere l'implementazione Close identica a Dispose e valutare di implementare il metodo IDisposable.Dispose in modo esplicito.

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

Tipi finalizzabili

I tipi finalizzabili sono tipi che estendono il criterio Dispose Basic eseguendo l'override del finalizzatore e fornendo il percorso del codice di finalizzazione nel metodo Dispose(bool).

I finalizzatori sono notoriamente difficili da implementare correttamente, principalmente perché non è possibile fare determinate supposizioni (normalmente valide) sullo stato del sistema durante l'esecuzione. Considerare attentamente le seguenti linee guida.

Notare che alcune delle linee guida sono applicabili non solo al metodo Finalize, ma a qualsiasi codice chiamato da un finalizzatore. Nel caso del criterio Dispose Basic definito in precedenza, ciò si riferisce alla logica eseguita all'interno di Dispose(bool disposing) quando il parametro disposing è false.

Se la classe di base è già finalizzabile e implementa il criterio Dispose Basic, non è indicato eseguire nuovamente l'override di Finalize. È invece consigliabile eseguire l'override del metodo Dispose(bool) per fornire logica di pulizia delle risorse aggiuntiva.

Il seguente codice illustra un esempio di tipo finalizzabile:

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 EVITARE di rendere i tipi finalizzabili.

Valutare attentamente casi in cui si ritiene sia necessario un finalizzatore. Le istanze con finalizzatori hanno un costo reale, sia in termini di prestazioni che di complessità del codice. Quando possibile, preferire l'uso di wrapper di risorse come SafeHandle per incapsulare le risorse non gestite; in questo caso, non sarà necessario un finalizzatore, poiché il wrapper è responsabile della propria pulizia risorse.

NON rendere finalizzabili i tipi valore.

Solo i tipi riferimento vengono effettivamente finalizzati da CLR; pertanto, qualsiasi tentativo di usare un finalizzatore su un tipo valore verrà ignorato. I compilatori C# e C++ seguono questa regola.

✓ RENDERE un tipo finalizzabile se esso è responsabile del rilascio di una risorsa non gestita che non dispone di un proprio finalizzatore.

Quando si implementa il finalizzatore, è sufficiente chiamare Dispose(false) e inserire tutta la logica di pulizia delle risorse all'interno del metodo Dispose(bool disposing).

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

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

✓ IMPLEMENTARE il criterio Dispose Basic in tutti i tipi finalizzabili.

Questo fornisce agli utenti del tipo un metodo per eseguire esplicitamente la pulizia deterministica delle stesse risorse per cui il finalizzatore è responsabile.

X NON accedere a oggetti finalizzabili nel percorso del codice del finalizzatore, poiché c’è un alto rischio che siano già stati finalizzati.

Ad esempio, un oggetto finalizzabile A che fa riferimento a un altro oggetto finalizzabile B non può usare attendibilmente B nel finalizzatore di A o viceversa. I finalizzatori vengono chiamati in ordine casuale (privo di una garanzia di ordine debole per la finalizzazione critica).

Inoltre, tenere presente che gli oggetti archiviati in variabili statiche verranno raccolti in determinati momenti durante lo scaricamento di un dominio applicazione o durante l'uscita dal processo. L'accesso a una variabile statica che fa riferimento a un oggetto finalizzabile (o che chiama un metodo statico che potrebbe fare uso di valori archiviati in variabili statiche) potrebbe non essere sicuro se Environment.HasShutdownStarted restituisce true.

✓ FARE IN MODO che il metodo Finalize sia protetto.

Gli sviluppatori C#, C++ e VB.NET non devono preoccuparsi di questo problema, poiché i compilatori aiutano ad applicare questa linea guida.

X NON far sfuggire eccezioni dalla logica del finalizzatore, eccetto errori critici di sistema.

Se un finalizzatore genera un’eccezione, CLR arresterà l'intero processo (in .NET Framework versione 2.0), impedendo che altri finalizzatori vengano eseguiti e che risorse vengano rilasciate in modo controllato.

✓ VALUTARE la creazione e l'uso di un oggetto finalizzabile critico (un tipo con una gerarchia di tipi contenente CriticalFinalizerObject) in situazioni in cui un l’esecuzione di un finalizzatore sia assolutamente necessaria, anche in caso di scaricamento forzato del dominio applicazione o di interruzioni del thread.

Parti protette da copyright © 2005, 2009 Microsoft Corporation. Tutti i diritti sono riservati.

Ristampato con l'autorizzazione di Pearson Education, Inc. da Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2a edizione di Krzysztof Cwalina and Brad Abrams, pubblicato il 22 ottobre 2008 da Addison-Wesley Professional nella collana Microsoft Windows Development Series.

Vedi anche