La clausola finally in C#
- Ad un blocco
try
può seguire uno o più blocchicatch
e al massimo un bloccofinally
. - Il blocco
finally
contiene codice che verrà sempre e comunque eseguito dopo il bloccotry
, indipendentemente dal fatto che sia stata lanciata un'eccezione o meno. - Grazie al blocco
finally
, possiamo garantire che le risorse vengano sempre rilasciate, anche se si verifica un'eccezione o viene eseguita un'istruzionereturn
all'interno del bloccotry
. - In questo modo, possiamo evitare perdite di risorse e garantire che il nostro codice sia robusto e affidabile.
La clausola finally
Ogni blocco try
può contenere un rispettivo blocco finally
.
Il codice all'interno del blocco finally
viene sempre eseguito, indipendentemente da come il flusso del programma esce dal blocco try
. Questo garantisce che il blocco finally
venga eseguito anche se viene lanciata un'eccezione o viene eseguita un'istruzione return
all'interno del blocco try
.
Casi in cui il blocco finally
non viene eseguito
Il codice nel blocco finally
non verrà eseguito se durante l'esecuzione del blocco try
, il sistema CLR viene terminato inaspettatamente, ad esempio se il programma viene interrotto attraverso Task Manager di Windows. Oppure se in Linux il processo viene terminato con un comando come kill -9
.
La sintassi base del blocco finally
è fornita di seguito:
try
{
// Codice che potrebbe o non potrebbe causare un'eccezione
}
finally
{
// Il codice qui verrà sempre eseguito
}
Ogni blocco try
può avere zero o più blocchi catch
e al massimo un blocco finally
.
È possibile avere più blocchi catch
e un blocco finally
nello stesso costrutto try-catch-finally
.
try
{
// Codice che potrebbe causare un'eccezione
}
catch (EccezioneTipo1 e1)
{
// Codice che gestisce l'eccezione di tipo 1
}
catch (EccezioneTipo2 e2)
{
// Codice che gestisce l'eccezione di tipo 2
}
catch (EccezioneTipo3 e3)
{
// Codice che gestisce l'eccezione di tipo 3
}
catch (Exception e)
{
// Codice che gestisce un'altra eccezione
}
finally
{
// Questo codice verrà sempre eseguito
}
Quando usare il blocco finally
In molte applicazioni dobbiamo lavorare con risorse esterne per i nostri programmi.
Esempi di risorse esterne includono file, connessioni di rete, elementi grafici, pipe e stream da o verso diversi dispositivi hardware (come stampanti, lettori di smart card e altri).
Quando trattiamo con tali risorse esterne, è di importanza critica liberare le risorse il prima possibile quando la risorsa non è più necessaria.
Ad esempio, quando apriamo un file per leggere il suo contenuto (diciamo per caricare un'immagine JPG), dobbiamo chiudere il file subito dopo aver letto il contenuto. Se lasciamo il file aperto, il sistema operativo impedirà ad altri utenti e applicazioni di effettuare certe operazioni sul file. Un esempio tipico di questa situazione è quando non si riesce a eliminare qualche directory o un file perché è in uso da un processo in esecuzione.
Il blocco finally
è inestimabile quando abbiamo bisogno di liberare una risorsa esterna o fare qualsiasi altra pulizia. Il blocco finally
garantisce che le operazioni di pulizia non verranno accidentalmente saltate a causa di un'eccezione inaspettata o a causa dell'esecuzione di return
, continue
o break
.
Poiché la gestione appropriata delle risorse è un concetto importante nella programmazione, la esamineremo in maggior dettaglio.
Il problema della pulizia delle risorse
Analizziamo un esempio di codice che mostra il problema della pulizia delle risorse.
Nel nostro esempio, vogliamo leggere un file. Per raggiungere questo obiettivo, abbiamo un lettore che deve essere chiuso quando il file è stato letto. Il modo migliore per fare questo è circondare le righe che usano uno StreamReader
in un blocco try
-finally
.
Ecco un esempio di codice che legge una riga da un file:
static void LeggiFile(string nomeFile)
{
TextReader lettore = new StreamReader(nomeFile);
string riga = lettore.ReadLine();
Console.WriteLine(riga);
lettore.Close();
}
Qual è il problema con questo codice?
Beh, quello che il codice dovrebbe fare è aprire uno StreamReader
, leggere i dati e poi chiudere il reader prima che il metodo ritorni. Quest'ultima parte è un problema perché il metodo potrebbe terminare in uno di diversi modi:
- Un'eccezione potrebbe essere lanciata quando il reader viene inizializzato (ad esempio se il file non esiste).
- Durante la lettura del file, potrebbe sorgere un'eccezione (si immagini un file su un dispositivo di rete remoto che va offline durante la lettura del file).
- Un'istruzione
return
potrebbe essere eseguita prima che il reader sia chiuso (nel nostro esempio banale questo sarebbe ovvio ma non è sempre così evidente). - Tutto va come previsto e il metodo viene eseguito normalmente.
Quindi il nostro metodo come scritto nell'esempio sopra ha un difetto critico: chiuderà il reader solo nell'ultimo scenario.
In tutti gli altri casi, il codice che chiude il reader non verrà eseguito. E se questo codice è all'interno di un ciclo, le cose diventano ancora più complesse poiché devono essere considerati anche le istruzioni continue
e break
.
Soluzione semplice
Nella sezione precedente abbiamo spiegato il difetto fondamentale dell'esempio di lettura e scrittura di un file. Se si verifica un errore durante l'apertura o la lettura del file, lasceremo il file aperto.
Per risolvere questo problema, possiamo utilizzare il costrutto try-finally
.
Discuteremo prima il caso in cui abbiamo una sola risorsa da pulire (in questo caso un file). Poi daremo un esempio quando abbiamo due o più risorse.
La chiusura di uno stream di file può essere effettuata utilizzando il seguente modello:
static void LeggiFile(string nomeFile)
{
TextReader lettore = null;
try
{
lettore = new StreamReader(nomeFile);
string linea = lettore.ReadLine();
Console.WriteLine(linea);
}
finally
{
// Chiudere sempre "lettore" (se è stato aperto)
if (lettore != null)
{
lettore.Close();
}
}
}
In questo esempio dichiariamo prima la variabile lettore
, e poi inizializziamo il TextReader
in un blocco try
. Quindi nel blocco finally
chiudiamo il lettore. Qualunque cosa accada durante l'inizializzazione del TextReader
o durante la lettura, abbiamo la garanzia che il file verrà chiuso.
Se c'è un problema nell'inizializzazione del lettore (diciamo che il file manca), allora lettore
rimarrà null
e questo è il motivo per cui facciamo un controllo per verificare se lettore
è null
nel blocco finally prima di chiamare Close()
. Se il valore è effettivamente null
, allora il lettore non è stato inizializzato e non c'è bisogno di chiuderlo. Il codice sopra garantisce che se il file è stato aperto, allora verrà chiuso indipendentemente da come il metodo esce.
L'esempio sopra dovrebbe, in linea di principio, gestire correttamente tutte le eccezioni relative all'apertura e all'inizializzazione del lettore (come FileNotFoundException
). Nel nostro esempio, queste eccezioni non vengono gestite e vengono semplicemente propagate al chiamante.
Abbiamo scelto gli stream di file per il nostro esempio per liberare le risorse ma lo stesso principio si applica a tutte le risorse che richiedono una pulizia appropriata. Queste potrebbero essere connessioni remote, risorse del sistema operativo, connessioni al database e così via.
Soluzione migliorata
Mentre la soluzione precedente è corretta, è inutilmente complessa. Diamo un'occhiata a una versione semplificata:
static void LeggiFile(string nomeFile)
{
TextReader lettore = new StreamReader(nomeFile);
try
{
string linea = lettore.ReadLine();
Console.WriteLine(linea);
}
finally
{
lettore.Close();
}
}
Questo codice ha il vantaggio di essere più semplice e breve. Evitiamo la dichiarazione preliminare della variabile lettore
e il controllo per null
nel blocco finally
. Il controllo null
ora non è necessario perché l'inizializzazione del lettore è al di fuori del blocco try
e se si verifica un'eccezione durante l'inizializzazione, il blocco finally
non verrà eseguito affatto.
Questo codice è più pulito, breve e chiaro ed è noto come dispose pattern. Tuttavia, si noti che in questo modo l'eccezione risalirà al metodo che chiama LeggiFile()
.
Pulizia di Risorse Multiple
A volte abbiamo bisogno di liberare più di una risorsa. È una buona pratica liberare le risorse in ordine inverso rispetto alla loro allocazione.
Possiamo usare lo stesso approccio descritto sopra, annidando i blocchi try-finally
uno dentro l'altro:
static void LeggiFile(string nomeFile)
{
Resource r1 = new Resource1();
try
{
Resource r2 = new Resource2();
try
{
// Usa r1 e r2
}
finally
{
r2.Release();
}
}
finally
{
r1.Release();
}
}
Un'altra opzione è dichiarare tutte le risorse in anticipo e quindi effettuare la pulizia in un singolo blocco finally
con i rispettivi controlli null
:
static void LeggiFile(string nomeFile)
{
Resource r1 = null;
Resource r2 = null;
try
{
r1 = new Resource1();
r2 = new Resource2();
// Usa r1 e r2
}
finally
{
if (r1 != null)
{
r1.Release();
}
if (r2 != null)
{
r2.Release();
}
}
}
Entrambe queste opzioni sono corrette ed entrambe vengono applicate a seconda della situazione e della preferenza del programmatore. Il secondo approccio è un po' più rischioso perché se si verifica un'eccezione nel blocco finally
, alcune delle risorse non verranno pulite. Nell'esempio sopra, se viene lanciata un'eccezione durante r1.Release()
, r2
non verrà pulita. Se usiamo la prima opzione, non c'è questo problema ma il codice è un po' più lungo.