Gestione delle Eccezioni in C#

Rendere i programmi robusti di fronte a errori e situazioni impreviste

⚠️ Introduzione alle Eccezioni

Le eccezioni sono eventi che si verificano durante l’esecuzione di un programma che
interrompono il normale flusso di istruzioni. Rappresentano errori o situazioni anomale che, se non gestite,
possono causare l’arresto improvviso dell’applicazione. La gestione delle eccezioni in C# permette di
anticipare questi problemi e risolverli in modo elegante.

💡 Perché Gestire le Eccezioni?

Immagina un programma che chiede all’utente di inserire un numero. Se l’utente inserisce del testo
invece di un numero, il programma potrebbe crashare improvvisamente. Con la gestione
delle eccezioni, puoi intercettare questo errore e chiedere gentilmente all’utente di riprovare,
mantenendo il programma in esecuzione.

🔧 Struttura Base di try-catch-finally

La gestione delle eccezioni in C# utilizza i blocchi try, catch e finally:

Sintassi Base
try 
{
    // Codice che potrebbe generare un'eccezione
} 
catch (TipoEccezione ex) 
{
    // Codice che gestisce l'eccezione
} 
finally 
{
    // Codice che viene eseguito SEMPRE, indipendentemente 
    // dal verificarsi di un'eccezione
}

📌 Funzionamento Step-by-Step

  1. Il codice all’interno del blocco try viene eseguito normalmente
  2. Se si verifica un’eccezione, l’esecuzione del blocco try viene interrotta
  3. Il controllo passa al primo blocco catch che corrisponde al tipo di eccezione
  4. Il blocco finally viene sempre eseguito (utile per pulizia risorse)
Esempio Pratico Semplice
try 
{
    Console.Write("Inserisci un numero: ");
    string input = Console.ReadLine();
    int numero = int.Parse(input);  // Potrebbe generare eccezione
    Console.WriteLine($"Hai inserito: {numero}");
} 
catch (FormatException) 
{
    Console.WriteLine("❌ Errore: devi inserire un numero valido!");
} 
finally 
{
    Console.WriteLine("✅ Operazione completata.");
}

🚨 Tipi di Eccezioni Comuni

NullReferenceException

Si verifica quando si tenta di accedere a un metodo o una proprietà di un oggetto che è null:

NullReferenceException – Esempio
string nome = null;

try 
{
    int lunghezza = nome.Length; // Genera NullReferenceException
    Console.WriteLine($"Lunghezza: {lunghezza}");
} 
catch (NullReferenceException ex) 
{
    Console.WriteLine($"❌ L'oggetto è null: {ex.Message}");
}

IndexOutOfRangeException

Si verifica quando si tenta di accedere a un indice non valido di un array:

IndexOutOfRangeException – Esempio
int[] numeri = { 1, 2, 3 };

try 
{
    int valore = numeri[5]; // Array ha solo indici 0, 1, 2
    Console.WriteLine(valore);
} 
catch (IndexOutOfRangeException ex) 
{
    Console.WriteLine($"❌ Indice non valido: {ex.Message}");
}

DivideByZeroException

Si verifica quando si tenta di dividere per zero:

DivideByZeroException – Esempio
try 
{
    int risultato = 10 / 0; // Impossibile dividere per zero
    Console.WriteLine(risultato);
} 
catch (DivideByZeroException ex) 
{
    Console.WriteLine($"❌ Impossibile dividere per zero: {ex.Message}");
}

FormatException

Si verifica quando si tenta di convertire una stringa in un formato non valido:

FormatException – Esempio
try 
{
    int numero = int.Parse("abc"); // "abc" non è un numero
    Console.WriteLine(numero);
} 
catch (FormatException ex) 
{
    Console.WriteLine($"❌ Formato non valido: {ex.Message}");
}

Eccezione Quando si Verifica Esempio Tipico
NullReferenceException Accesso a oggetto null string.Length su null
IndexOutOfRangeException Indice array non valido array[10] su array di 5 elementi
DivideByZeroException Divisione per zero 10 / 0
FormatException Conversione formato errato int.Parse("abc")
ArgumentException Argomento non valido Parametro fuori range

🎯 Gestione di Più Eccezioni

È possibile gestire diversi tipi di eccezioni con più blocchi catch.
Questo permette di fornire messaggi di errore specifici per ogni situazione.

Multipli Blocchi catch
try 
{
    Console.Write("Inserisci un numero: ");
    string input = Console.ReadLine();
    int numero = int.Parse(input);
    int risultato = 100 / numero;
    Console.WriteLine($"Risultato: {risultato}");
} 
catch (FormatException) 
{
    Console.WriteLine("❌ L'input non è un numero valido.");
} 
catch (DivideByZeroException) 
{
    Console.WriteLine("❌ Non è possibile dividere per zero.");
} 
catch (Exception ex) 
{
    // Cattura qualsiasi altra eccezione non gestita sopra
    Console.WriteLine($"❌ Errore generico: {ex.Message}");
} 
finally 
{
    Console.WriteLine("✅ Operazione completata.");
}

⚠️ IMPORTANTE: Ordine dei Blocchi catch

I blocchi catch devono essere disposti dal più specifico al più generale.
Se posizioni catch (Exception) prima di altri blocchi più specifici, questi non verranno
mai raggiunti perché Exception è la classe base per tutte le eccezioni.

Ordine Corretto vs Errato
// ✅ CORRETTO - Dal più specifico al più generale
try { /* ... */ }
catch (FormatException ex) { /* ... */ }
catch (DivideByZeroException ex) { /* ... */ }
catch (Exception ex) { /* ... */ }

// ❌ ERRATO - Exception cattura tutto
try { /* ... */ }
catch (Exception ex) { /* ... */ }
catch (FormatException ex) { /* QUESTO NON VERRÀ MAI ESEGUITO */ }
catch (DivideByZeroException ex) { /* QUESTO NON VERRÀ MAI ESEGUITO */ }

📤 Propagazione delle Eccezioni

Se un’eccezione non viene gestita in un metodo, viene propagata al metodo chiamante.
Questo meccanismo permette di gestire gli errori a livelli superiori del programma.

Esempio di Propagazione
static void Main() 
{
    try 
    {
        MetodoA();
    } 
    catch (Exception ex) 
    {
        Console.WriteLine($"❌ Eccezione catturata in Main: {ex.Message}");
    }
}

static void MetodoA() 
{
    Console.WriteLine("Eseguo MetodoA...");
    MetodoB(); // L'eccezione verrà propagata a Main
}

static void MetodoB() 
{
    Console.WriteLine("Eseguo MetodoB...");
    throw new Exception("Errore critico in MetodoB!");
}

// Output:
// Eseguo MetodoA...
// Eseguo MetodoB...
// ❌ Eccezione catturata in Main: Errore critico in MetodoB!

🔄 Flusso di Propagazione

  1. Main() chiama MetodoA()
  2. MetodoA() chiama MetodoB()
  3. MetodoB() genera un’eccezione con throw
  4. L’eccezione risale a MetodoA() (non gestita)
  5. L’eccezione risale a Main() dove viene catturata

🎪 Lancio di Eccezioni Personalizzate

È possibile lanciare eccezioni esplicitamente con la parola chiave throw.
Questo è utile per validare input o parametri.

Throw per Validazione
public void Deposita(decimal importo) 
{
    if (importo <= 0) 
    {
        throw new ArgumentException("L'importo deve essere positivo");
    }
    
    saldo += importo;
    Console.WriteLine($"Depositati {importo}€. Saldo: {saldo}€");
}

// Utilizzo:
try 
{
    Deposita(-50); // Genera ArgumentException
} 
catch (ArgumentException ex) 
{
    Console.WriteLine($"❌ Errore: {ex.Message}");
}

Eccezioni Personalizzate

È anche possibile creare eccezioni personalizzate estendendo la classe Exception:

Classe Eccezione Personalizzata
public class SaldoInsufficienteException : Exception 
{
    public decimal SaldoAttuale { get; }
    public decimal ImportoRichiesto { get; }
    
    public SaldoInsufficienteException(string message, 
                                        decimal saldoAttuale, 
                                        decimal importoRichiesto) 
        : base(message) 
    {
        SaldoAttuale = saldoAttuale;
        ImportoRichiesto = importoRichiesto;
    }
}

// Utilizzo:
public void Preleva(decimal importo) 
{
    if (importo > saldo) 
    {
        throw new SaldoInsufficienteException(
            "Saldo insufficiente per il prelievo", 
            saldo, 
            importo);
    }
    
    saldo -= importo;
}

// Gestione:
try 
{
    Preleva(1000);
} 
catch (SaldoInsufficienteException ex) 
{
    Console.WriteLine($"❌ {ex.Message}");
    Console.WriteLine($"Saldo attuale: {ex.SaldoAttuale}€");
    Console.WriteLine($"Importo richiesto: {ex.ImportoRichiesto}€");
}

🚀 Esempio Pratico: Validazione Input Utente

Vediamo un esempio completo che dimostra come gestire le eccezioni per validare l'input utente
in modo robusto.

Program.cs - Validazione con Eccezioni
using System;

class Program 
{
    static void Main() 
    {
        bool inputValido = false;
        int eta = 0;
        
        Console.WriteLine("=== REGISTRAZIONE UTENTE ===\n");
        
        while (!inputValido) 
        {
            Console.Write("Inserisci la tua età: ");
            
            try 
            {
                string input = Console.ReadLine();
                eta = int.Parse(input);
                
                if (eta < 0 || eta > 120) 
                {
                    throw new ArgumentOutOfRangeException(
                        "L'età deve essere compresa tra 0 e 120");
                }
                
                inputValido = true;
                Console.WriteLine($"✅ Hai inserito: {eta} anni");
                
            } 
            catch (FormatException) 
            {
                Console.WriteLine("❌ Errore: devi inserire un numero intero.\n");
            } 
            catch (ArgumentOutOfRangeException ex) 
            {
                Console.WriteLine($"❌ Errore: {ex.Message}\n");
            } 
            catch (Exception ex) 
            {
                Console.WriteLine($"❌ Errore imprevisto: {ex.Message}\n");
            }
        }
        
        try 
        {
            string fascia = DeterminaFasciaEta(eta);
            Console.WriteLine($"\n📊 Appartieni alla fascia d'età: {fascia}");
        } 
        catch (Exception ex) 
        {
            Console.WriteLine($"❌ Errore nel determinare la fascia: {ex.Message}");
        }
    }
    
    static string DeterminaFasciaEta(int eta) 
    {
        if (eta < 0) 
        {
            throw new ArgumentException("L'età non può essere negativa");
        }
        
        if (eta <= 12) return "Bambino";
        if (eta <= 19) return "Adolescente";
        if (eta <= 65) return "Adulto";
        return "Anziano";
    }
}

💡 Punti Chiave dell'Esempio
  • Loop while continua fino a input valido
  • Gestione separata per FormatException e ArgumentOutOfRangeException
  • catch (Exception) finale per errori imprevisti
  • Validazione sia nel metodo principale che in DeterminaFasciaEta()

✨ Best Practices nella Gestione delle Eccezioni

1️⃣ Gestisci Solo Eccezioni che Puoi Risolvere

Non catturare eccezioni che non sei in grado di gestire in modo significativo.
A volte è meglio lasciare che l'eccezione si propaghi a un livello superiore dove
può essere gestita appropriatamente.

Esempio: Quando NON Catturare
// ❌ SCONSIGLIATO - Cattura e ignora
try 
{
    // Operazione critica
} 
catch (Exception) 
{
    // Non fare nulla - "swallow exception"
}

// ✅ CONSIGLIATO - Lascia propagare o gestisci
try 
{
    // Operazione critica
} 
catch (SpecificException ex) 
{
    // Log dell'errore
    Console.WriteLine($"Errore: {ex.Message}");
    // Eventualmente ri-lancia
    throw;
}

2️⃣ Non Usare Eccezioni per il Controllo di Flusso

Le eccezioni dovrebbero essere riservate a situazioni eccezionali, non per il controllo
del flusso normale del programma. Usa if/else o metodi come TryParse
quando possibile.

Esempio: Controllo di Flusso
// ❌ SCONSIGLIATO - Usa eccezioni per controllo
try 
{
    int numero = int.Parse(input);
} 
catch (FormatException) 
{
    numero = 0; // Valore di default
}

// ✅ CONSIGLIATO - Usa TryParse
int numero;
if (!int.TryParse(input, out numero)) 
{
    numero = 0; // Valore di default
}

3️⃣ Mantieni i Blocchi try il Più Piccoli Possibili

Includi nel blocco try solo il codice che potrebbe generare l'eccezione
che ti interessa gestire, non l'intero metodo. Questo rende più chiaro quale codice
può generare errori.

Esempio: Dimensione Blocco try
// ❌ SCONSIGLIATO - try troppo ampio
try 
{
    Console.Write("Inserisci numero: ");
    string input = Console.ReadLine();
    int numero = int.Parse(input);
    int risultato = Calcola(numero);
    Console.WriteLine($"Risultato: {risultato}");
} 
catch (FormatException) { /* ... */ }

// ✅ CONSIGLIATO - try mirato
Console.Write("Inserisci numero: ");
string input = Console.ReadLine();

int numero;
try 
{
    numero = int.Parse(input); // Solo questa riga può generare FormatException
} 
catch (FormatException) 
{
    Console.WriteLine("Input non valido");
    return;
}

int risultato = Calcola(numero);
Console.WriteLine($"Risultato: {risultato}");

4️⃣ Usa finally per la Pulizia delle Risorse

Utilizza il blocco finally per liberare risorse come file, connessioni
di rete o database, assicurandoti che vengano chiuse correttamente anche in caso di eccezioni.

Esempio: Pulizia Risorse
FileStream file = null;
try 
{
    file = new FileStream("dati.txt", FileMode.Open);
    // Operazioni sul file
} 
catch (IOException ex) 
{
    Console.WriteLine($"Errore I/O: {ex.Message}");
} 
finally 
{
    // Questa sezione viene SEMPRE eseguita
    if (file != null) 
    {
        file.Close();
        Console.WriteLine("File chiuso correttamente");
    }
}

// Ancora meglio: usa using statement
using (FileStream file = new FileStream("dati.txt", FileMode.Open)) 
{
    // Il file viene chiuso automaticamente
}

Best Practice Descrizione Beneficio
Specifico → Generale Ordina catch dal più specifico al più generale Gestione precisa degli errori
Usa TryParse Preferisci TryParse a Parse + try-catch Migliori performance
Blocchi try piccoli Include solo codice che può generare eccezioni Codice più chiaro e manutenibile
Messaggi descrittivi Fornisci messaggi di errore chiari Debugging più facile
Usa finally Pulizia risorse nel blocco finally Previene memory leak

💪 Esercizi Pratici

📝 Esercizio 1: Calcolatrice Sicura

Crea una calcolatrice che:

  • Chiede all'utente due numeri
  • Chiede l'operazione da eseguire (+, -, *, /)
  • Gestisce tutti i possibili errori (formato, divisione per zero, ecc.)
  • Permette di ripetere l'operazione

📝 Esercizio 2: Gestore Password

Crea un sistema che valida password con le seguenti regole:

  • Lunghezza minima 8 caratteri
  • Almeno una maiuscola
  • Almeno una minuscola
  • Almeno un numero
  • Lancia eccezioni personalizzate per ogni regola violata

🎯 Riepilogo Lezione

Hai imparato:

  • Struttura try-catch-finally
  • Tipi di eccezioni comuni
  • Gestione multipla con catch
  • Propagazione eccezioni
  • Lancio e creazione eccezioni
Concetti chiave:

  • Catch: specifico → generale
  • Usa TryParse quando possibile
  • Finally per pulizia risorse
  • Messaggi di errore chiari
  • Non usare per controllo flusso

Lezione 10 del Corso C# Completo

Torna in alto