La gestione delle eccezioni in C#

La gestione delle eccezioni in C#

Nessun programma reale è immune dagli imprevisti: un file può non esistere, una connessione di rete può cadere, un input dell'utente può avere un formato inatteso. In C# questi eventi anomali vengono rappresentati dalle eccezioni, oggetti che interrompono il normale flusso di esecuzione per segnalare che qualcosa è andato storto. Apre questa seconda serie, di livello intermedio, un argomento trasversale a ogni applicazione: come intercettare, gestire e sollevare le eccezioni in modo corretto e leggibile.

Che cos'è un'eccezione

Un'eccezione è un oggetto che descrive una condizione di errore. Quando si verifica un problema, il runtime crea un'eccezione e la propaga risalendo la catena delle chiamate finché non trova un gestore in grado di occuparsene. Se nessun gestore la intercetta, il programma termina in modo anomalo. Tutte le eccezioni derivano dalla classe base Exception, che espone informazioni utili come il messaggio descrittivo e lo stack delle chiamate.

Il blocco try-catch

Il costrutto fondamentale per gestire le eccezioni è il blocco try-catch. Il codice potenzialmente problematico si racchiude in un blocco try; se al suo interno viene sollevata un'eccezione, l'esecuzione salta immediatamente al blocco catch corrispondente.

try
{
    int[] numbers = { 1, 2, 3 };
    Console.WriteLine(numbers[10]); // indice fuori dai limiti
}
catch (IndexOutOfRangeException ex)
{
    // Gestione dell'errore specifico
    Console.WriteLine($"Errore di indice: {ex.Message}");
}

Specificare il tipo di eccezione nel catch permette di intercettare solo un determinato genere di errore. È buona pratica catturare eccezioni il più specifiche possibile, evitando di intercettare genericamente Exception a meno che non sia davvero necessario.

Catturare più tipi di eccezione

Un singolo blocco try può essere seguito da più blocchi catch, ciascuno dedicato a un tipo diverso. Il runtime valuta i blocchi nell'ordine in cui sono scritti e sceglie il primo compatibile, per cui i tipi più specifici vanno posti prima di quelli più generali.

try
{
    string input = Console.ReadLine();
    int value = int.Parse(input);
    int result = 100 / value;
    Console.WriteLine(result);
}
catch (FormatException)
{
    Console.WriteLine("Il formato del numero non e valido");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Impossibile dividere per zero");
}
catch (Exception ex)
{
    // Rete di sicurezza per qualsiasi altro errore
    Console.WriteLine($"Errore imprevisto: {ex.Message}");
}

Il blocco finally

Il blocco finally, opzionale, contiene codice che viene eseguito in ogni caso, sia che si verifichi un'eccezione sia che tutto proceda regolarmente. È il luogo ideale per rilasciare risorse, come chiudere un file o una connessione, garantendo che la pulizia avvenga sempre.

var resource = new StreamReader("file.txt");
try
{
    string content = resource.ReadToEnd();
    Console.WriteLine(content);
}
catch (IOException ex)
{
    Console.WriteLine($"Errore di lettura: {ex.Message}");
}
finally
{
    // Eseguito sempre, anche in caso di eccezione
    resource.Dispose();
}

Sollevare un'eccezione

Oltre a gestire le eccezioni, spesso è necessario sollevarne di proprie quando si rileva una condizione non valida. Si usa la parola chiave throw seguita da un'istanza di eccezione. Scegliere il tipo più appropriato aiuta chi userà il codice a capire la natura del problema.

static void SetAge(int age)
{
    if (age < 0)
    {
        // Segnala un argomento non valido
        throw new ArgumentException("L'eta non puo essere negativa", nameof(age));
    }
    Console.WriteLine($"Eta impostata a {age}");
}

Quando si intercetta un'eccezione ma non la si può gestire completamente, la si può rilanciare per farla risalire. Usare throw da solo, senza specificare l'oggetto, preserva lo stack trace originale, mentre throw ex lo azzererebbe, perdendo informazioni preziose per il debug.

catch (Exception ex)
{
    // Registra l'errore e lo rilancia preservando lo stack trace
    Console.WriteLine("Registro l'errore");
    throw;
}

Il filtro when

C# consente di aggiungere una condizione a un blocco catch tramite la clausola when. Il gestore interviene solo se la condizione è vera, permettendo di distinguere casi diversi dello stesso tipo di eccezione senza intrecciare la logica con degli if.

try
{
    ProcessRequest();
}
catch (HttpRequestException ex) when (ex.StatusCode == 404)
{
    Console.WriteLine("Risorsa non trovata");
}
catch (HttpRequestException ex) when (ex.StatusCode == 500)
{
    Console.WriteLine("Errore interno del server");
}

Eccezioni personalizzate

Quando i tipi di eccezione standard non descrivono adeguatamente un errore del dominio applicativo, è possibile definire eccezioni personalizzate derivando dalla classe Exception. Questo rende il codice più espressivo e permette a chi lo usa di intercettare esattamente le condizioni che interessano.

// Eccezione specifica del dominio applicativo
class InsufficientFundsException : Exception
{
    public decimal RequestedAmount { get; }

    public InsufficientFundsException(decimal requestedAmount, string message)
        : base(message)
    {
        RequestedAmount = requestedAmount;
    }
}

// Utilizzo
static void Withdraw(decimal balance, decimal amount)
{
    if (amount > balance)
    {
        throw new InsufficientFundsException(
            amount, "Fondi insufficienti per il prelievo");
    }
}

Buone pratiche

La gestione delle eccezioni è potente ma va usata con criterio. Alcuni principi da tenere presenti:

  • Non usare le eccezioni per controllare il flusso normale del programma: sono costose e vanno riservate a condizioni realmente eccezionali.
  • Preferire i metodi come TryParse, che segnalano il fallimento senza sollevare eccezioni, quando l'errore è una possibilità prevista.
  • Catturare solo le eccezioni che si è effettivamente in grado di gestire, lasciando propagare le altre.
  • Non sopprimere mai un'eccezione con un blocco catch vuoto: si nasconderebbe il problema senza risolverlo.

Conclusione

Abbiamo visto come C# rappresenta gli errori tramite le eccezioni e come gestirle con try, catch e finally, intercettando tipi specifici ed eseguendo sempre la pulizia delle risorse. Abbiamo imparato a sollevare eccezioni con throw, a rilanciarle preservando lo stack trace, a filtrarle con when e a definire eccezioni personalizzate per il dominio applicativo. Nel prossimo articolo affronteremo i generics, il meccanismo che permette di scrivere codice riutilizzabile e indipendente dal tipo.