Menu

Istruzione using in C#: IDisposable, Dispose e using var

L'istruzione using garantisce che Dispose venga chiamato su file, stream, connessioni e altri oggetti IDisposable, anche quando viene lanciata un'eccezione. Scopri in cosa viene compilata, l'ordine in cui gli oggetti vengono eliminati, la dichiarazione using di C# 8 e come implementare IDisposable nella tua classe.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Alcuni oggetti contengono risorse che il garbage collector non gestisce: file aperti, socket di rete, connessioni al database, handle del sistema operativo. Implementano IDisposable, e rilasci la risorsa chiamando Dispose(). L'istruzione using lo chiama per te in un punto preciso, anche se viene lanciata un'eccezione.

Questa pagina parla dell'istruzione using. Le righe using System; all'inizio di un file sono una funzionalità diversa, la direttiva using, che importa un namespace.

Un blocco using

using (declaration) { body } crea l'oggetto, esegue il corpo, poi chiama Dispose. Una piccola classe che stampa da Dispose mostra esattamente quando succede:

Output:

open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled

Il secondo blocco lancia un'eccezione, e "close api" viene comunque stampato, prima che il catch gestisca l'eccezione. Questa garanzia è il motivo per usare using invece di chiamare tu Dispose() sull'ultima riga: una chiamata manuale viene saltata da qualsiasi eccezione lanciata prima.

In cosa viene compilato using

Il compilatore espande un blocco using in un try/finally. Queste due forme sono equivalenti:

using (var writer = new StreamWriter("log.txt"))
{
    writer.WriteLine("started");
}

// is compiled as:
{
    var writer = new StreamWriter("log.txt");
    try
    {
        writer.WriteLine("started");
    }
    finally
    {
        if (writer != null) ((IDisposable)writer).Dispose();
    }
}

Da questa espansione seguono tre dettagli. La variabile ha come ambito il blocco ed è di sola lettura al suo interno (non puoi riassegnare writer). L'oggetto deve implementare IDisposable, altrimenti il codice non compila. E un valore null è consentito: Dispose viene semplicemente saltato, il che è comodo quando una factory potrebbe non restituire nulla.

File e stream

Le classi per file e stream sono il caso di tutti i giorni. Uno StreamWriter tiene il testo in un buffer in memoria, e Dispose scarica il buffer sul disco e chiude l'handle del file:

Output:

id,total
1001,59.90
1002,12.50

Senza Dispose, le ultime righe del writer potrebbero essere ancora nel suo buffer, e su Windows l'handle aperto impedisce ad altri programmi (e al tuo prossimo File.Delete) di toccare il file. Metodi di supporto come File.WriteAllText e File.ReadAllLines aprono ed eliminano internamente, quindi non hanno bisogno di using. Vedi file per saperne di più.

Più risorse: ordine di eliminazione

Impila istruzioni using senza graffe tra di loro per aprire più risorse in un solo blocco. Vengono eliminate in ordine inverso rispetto alla creazione:

Output:

acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a

L'ordine inverso è quello di cui hanno bisogno le risorse stratificate: un writer avvolge un buffer che avvolge un file, quindi il writer deve scaricare nel buffer prima che il buffer scriva nel file, prima che il file si chiuda.

La dichiarazione using (C# 8)

C# 8 ha aggiunto una forma senza graffe. La variabile viene eliminata quando termina l'ambito che la contiene, di solito la fine del metodo:

static void ExportReport(string path, IEnumerable<string> rows)
{
    using var writer = new StreamWriter(path);
    writer.WriteLine("Report");

    foreach (string row in rows)
    {
        writer.WriteLine(row);
    }
} // writer.Dispose() runs here, at the end of the method

Toglie un livello di indentazione, cosa che conta quando un metodo usa due o tre risorse. La regola sull'ordine di eliminazione è la stessa: le dichiarazioni nello stesso ambito vengono eliminate in ordine inverso. Il compromesso è che la risorsa resta aperta fino alla fine dell'ambito, quindi quando un file va chiuso prima che il metodo faccia altro lavoro, tieni la forma a blocco oppure metti la dichiarazione in un suo blocco { }.

Per gli oggetti la cui pulizia è asincrona (IAsyncDisposable, come molte connessioni al database e molti stream), C# 8 ha anche await using, che attende DisposeAsync() nello stesso punto.

Implementare IDisposable nella tua classe

Implementa IDisposable quando la tua classe possiede qualcosa di eliminabile (ha creato uno stream, un timer, una connessione) e quindi deve a sua volta eliminarlo. Nel caso comune si tratta di un metodo breve:

Output:

1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException

Le regole che segue: Dispose elimina tutto ciò che l'oggetto possiede; chiamarlo più di una volta non fa nulla la seconda volta; e usare l'oggetto dopo l'eliminazione lancia ObjectDisposedException. using (log) mostra anche che il blocco può prendere una variabile esistente invece di una dichiarazione.

Il "dispose pattern" più lungo, con un metodo protected virtual void Dispose(bool disposing) e un finalizzatore, serve solo quando una classe contiene direttamente un handle non gestito (un puntatore grezzo proveniente da codice nativo). Nel .NET moderno, avvolgi invece questi handle in una sottoclasse di SafeHandle, e alla tua classe basta la versione semplice qui sopra.

Errori comuni

  • Restituire un oggetto creato in un blocco using. Viene eliminato quando il blocco termina, quindi chi chiama riceve uno stream chiuso. Restituisci i dati, oppure lascia che sia chi chiama a gestire lo using.
  • Dimenticare using su stream e writer. I dati nel buffer possono andare persi e i file restano bloccati.
  • Eliminare un oggetto condiviso. Elimina solo ciò che possiedi. Un HttpClient di lunga durata, per esempio, è pensato per essere condiviso e riusato, non per essere creato ed eliminato a ogni richiesta.
  • Usare un oggetto dopo il blocco. I metodi di un oggetto eliminato lanciano ObjectDisposedException.
  • Affidarsi al garbage collector. Libera la memoria, non file o socket, e non chiama mai Dispose.

Domande frequenti

Cosa fa l'istruzione using in C#?

using (var x = ...) { ... } chiama x.Dispose() quando il blocco termina, che termini normalmente, tramite return o perché è stata lanciata un'eccezione. Il compilatore la trasforma in un try/finally con la chiamata a Dispose nel finally, così la risorsa viene rilasciata in un punto preciso invece che quando capita che giri il garbage collector.

Cos'è IDisposable in C#?

IDisposable è un'interfaccia con un solo metodo, void Dispose(). Una classe la implementa quando contiene qualcosa che va rilasciato esplicitamente: un handle di file, un socket di rete, una connessione al database, un timer. Qualsiasi oggetto il cui tipo implementa IDisposable va eliminato quando hai finito di usarlo, di solito con using.

Qual è la differenza tra using e using var?

using (var x = ...) { } elimina l'oggetto alla fine del suo blocco. La dichiarazione using using var x = ...; (C# 8 e successivi) non ha un blocco proprio: elimina l'oggetto quando termina l'ambito che la contiene, di solito la fine del metodo. Risparmia un livello di annidamento quando una risorsa deve vivere per il resto del metodo.

In che ordine vengono eliminati più oggetti using?

In ordine inverso rispetto alla creazione. Con using (var a = ...) using (var b = ...) { }, viene eliminato prima b e poi a. È l'ordine che vuoi quando b dipende da a, come uno StreamWriter che avvolge un FileStream.

Il garbage collector chiama Dispose?

No. Il garbage collector libera la memoria, e può eseguire un finalizzatore in un momento successivo imprevedibile, ma non chiama mai Dispose. Un file non eliminato può restare bloccato, e i suoi dati nel buffer potrebbero non essere mai scritti. È per questo che esiste using.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA