La clausola finally in C#

Concetti Chiave
  • Ad un blocco try può seguire uno o più blocchi catch e al massimo un blocco finally.
  • Il blocco finally contiene codice che verrà sempre e comunque eseguito dopo il blocco try, 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'istruzione return all'interno del blocco try.
  • 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.

Nota

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.