Menu

Lanciare eccezioni in C#: guard clause, throw expression, eccezioni personalizzate

Come e quando lanciare eccezioni in C#. Impara l'istruzione throw, quale tipo di eccezione predefinito si adatta a quale errore, le throw expression con ?? e ?:, i metodi di supporto ThrowIfNull, e come scrivere una classe di eccezione personalizzata con dati propri e un'eccezione interna.

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

Catturare le eccezioni è metà della gestione degli errori; l'altra metà è lanciare quella giusta. Un'eccezione scelta bene dice a chi chiama esattamente cosa è andato storto e se è stato un suo errore o dipende dallo stato del programma. Questa pagina copre il lato del lancio; try catch copre la gestione.

L'istruzione throw e le guard clause

throw accetta un oggetto eccezione. L'esecuzione del metodo si ferma lì, e l'eccezione cerca un gestore risalendo lo stack delle chiamate. L'uso più comune è una guard clause: controlli all'inizio di un metodo che respingono l'input non valido prima che venga fatto qualsiasi lavoro.

Output:

ArgumentOutOfRangeException for parameter 'amount'
InvalidOperationException: The account is frozen.
Balance: 100

nameof(amount) produce la stringa "amount" e resta corretto se il parametro viene rinominato. Le eccezioni sugli argomenti la memorizzano in ParamName, che strumenti e log usano per indicare l'argomento sbagliato.

Le guard clause mantengono semplice il resto del metodo: superati i controlli, il codice può dare per scontato che l'input sia valido. Inoltre falliscono nel punto dell'errore, invece di lasciare che un valore sbagliato vada avanti e causi una NullReferenceException incomprensibile tre metodi più in là.

Quale tipo di eccezione lanciare

Riusa un tipo predefinito quando descrive la situazione; chi chiama sa già come gestirlo.

SituazioneLancia
Un argomento obbligatorio è nullArgumentNullException
Un argomento è fuori dall'intervallo consentito (quantità negativa, indice oltre la fine)ArgumentOutOfRangeException
Un argomento non è valido in un altro modo (nome vuoto, ID malformato)ArgumentException
La chiamata non è valida nello stato attuale dell'oggettoInvalidOperationException
L'operazione non è mai supportata da questo tipo (Add su una collezione di sola lettura)NotSupportedException
Il metodo non è ancora stato scrittoNotImplementedException
Un oggetto è stato usato dopo DisposeObjectDisposedException
Un'operazione a tempo ha esaurito il tempoTimeoutException

Il confine tra le prime tre righe e InvalidOperationException sta in chi deve cambiare qualcosa. Un'eccezione sugli argomenti dice "chiamalo in modo diverso". InvalidOperationException dice "la chiamata andava bene, ma non adesso".

Non lanciare direttamente Exception, SystemException o ApplicationException: chi chiama non può catturarle senza catturare anche tutto il resto. Non lanciare nemmeno tu NullReferenceException, IndexOutOfRangeException o StackOverflowException; il runtime le riserva ai bug veri.

Throw expression

Prima di C# 7, throw era solo un'istruzione. Da C# 7 può comparire anche come espressione in tre punti, il che trasforma i controlli più comuni in una sola riga:

Output:

Ana <ana@example.com>
Null: name
ArgumentException: email

Nota che ArgumentNullException deriva da ArgumentException, quindi un catch (ArgumentException) messo per primo catturerebbe anche il caso null. Per lo stesso motivo l'ordine delle clausole catch conta quando gestisci entrambe.

ThrowIfNull e simili (.NET 6 e successivi)

Il .NET moderno aggiunge metodi statici di supporto che scrivono per te il controllo e il lancio, catturando automaticamente il nome del parametro:

public void Ship(Order order, int quantity, string address)
{
    ArgumentNullException.ThrowIfNull(order);                   // .NET 6
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // .NET 8
    ArgumentException.ThrowIfNullOrWhiteSpace(address);          // .NET 8
    ObjectDisposedException.ThrowIf(disposed, this);             // .NET 7

    // ...
}

Si comportano come l'if e il throw scritti a mano, e mantengono ogni guard clause su una sola riga. Sulle piattaforme più vecchie, scrivi la forma con if mostrata prima.

Scrivere una classe di eccezione personalizzata

Crea un tuo tipo di eccezione quando chi chiama deve catturare questo errore specifico separatamente, o quando il gestore ha bisogno di dati che una stringa di messaggio non riesce a trasportare bene.

Output:

Cannot withdraw 25 from a balance of 15.
Short by 10

Le convenzioni:

  • Il nome finisce con Exception.
  • Deriva da Exception (o da un tipo predefinito più specifico quando ne è un caso particolare, come InvalidOperationException).
  • Ha i tre costruttori standard: senza argomenti, con messaggio, e con messaggio più eccezione interna. Aggiungi i tuoi costruttori oltre a questi.
  • I dati aggiuntivi vanno in proprietà di sola lettura, impostate nel costruttore. Un gestore può così agire su e.Requested invece di analizzare il messaggio.

Avvolgere con un'eccezione interna

Quando un errore di basso livello deve emergere come errore di livello più alto, avvolgilo. L'originale viene conservato come InnerException, quindi nessuna informazione va persa:

Output:

Setting 'port' must be a number, got '80a'.
Caused by: FormatException

Ora chi chiama ragiona in termini di configurazione, che capisce, e registrare e.ToString() nel log stampa l'intera catena, compresa la FormatException e il suo stack trace. Avvolgi solo quando aggiungi significato; avvolgere ogni eccezione in una generica MyAppException costringe solo i gestori a scavare in InnerException.

Lanciare un'eccezione o restituire un risultato

Le eccezioni servono per gli errori che chi chiama non si aspetta durante il normale funzionamento. Per gli esiti di routine, come una ricerca che spesso non trova nulla o un input dell'utente che spesso non è valido, la convenzione di .NET è il pattern Try: restituire bool e riconsegnare il valore tramite un parametro out.

public bool TryWithdraw(decimal amount, out string error)
{
    if (amount > Balance) { error = "Insufficient funds."; return false; }
    Balance -= amount;
    error = null;
    return true;
}

Molti tipi offrono entrambe le forme: int.Parse lancia un'eccezione, int.TryParse restituisce false; dict[key] lancia un'eccezione, dict.TryGetValue restituisce false. Lanciare un'eccezione costa molto più che restituire un valore, quindi non dovrebbe stare su un percorso eseguito migliaia di volte al secondo. Vedi ref e out per i parametri out.

Scrivere buoni messaggi

Il messaggio di un'eccezione viene letto da uno sviluppatore che guarda un log. Fai in modo che dica cosa era sbagliato e, quando è sicuro farlo, il valore incriminato: "Quantity must be between 1 and 99, got 0." è meglio di "Invalid input.". Scrivi frasi complete, e tieni fuori dai messaggi segreti come password e token, perché finiscono nei file di log.

Errori comuni

  • Lanciare Exception in sé. Chi chiama non può catturarla in modo selettivo; usa un tipo specifico.
  • Passare il messaggio al posto del nome del parametro. new ArgumentNullException("name") accetta il nome del parametro; il messaggio viene per secondo.
  • Nomi di parametro scritti a mano. Usa nameof(param) così restano corretti dopo una rinomina.
  • Eccezioni personalizzate senza un significato in più. Se un tipo predefinito va bene, usalo.
  • Perdere l'errore originale quando lo avvolgi. Passalo sempre come eccezione interna.

Domande frequenti

Come lancio un'eccezione in C#?

Crea un oggetto eccezione e lancialo: throw new ArgumentException("Amount must be positive", nameof(amount));. L'esecuzione si ferma su quella riga e l'eccezione risale lo stack delle chiamate fino al catch corrispondente più vicino. Scegli il tipo predefinito più specifico che descrive il problema, oppure un tipo personalizzato quando chi chiama deve gestire questo caso separatamente.

Come creo un'eccezione personalizzata in C#?

Deriva da Exception una classe il cui nome finisce con Exception, e dalle i costruttori standard: uno senza argomenti, uno che accetta un messaggio e uno che accetta un messaggio e un'eccezione interna, ognuno dei quali chiama il costruttore base(...) corrispondente. Aggiungi proprietà di sola lettura per i dati che servono a chi la gestisce, come l'ID di un ordine o un saldo.

Quando lanciare ArgumentException e quando InvalidOperationException?

Lancia una ArgumentException (o ArgumentNullException / ArgumentOutOfRangeException) quando chi chiama ha passato un valore sbagliato: la soluzione è chiamare il metodo in modo diverso. Lancia InvalidOperationException quando gli argomenti vanno bene ma l'oggetto è nello stato sbagliato per quella chiamata, come leggere da una connessione chiusa o prelevare da un conto congelato.

Cos'è una throw expression in C#?

Da C# 7, throw si può usare come espressione in tre punti: dopo ??, come uno dei due rami di ?:, e come corpo di un membro con corpo di espressione o di una lambda. Per esempio _name = name ?? throw new ArgumentNullException(nameof(name)); assegna o lancia in una sola riga.

Cosa fa ArgumentNullException.ThrowIfNull?

È un metodo statico di supporto aggiunto in .NET 6: ArgumentNullException.ThrowIfNull(customer); lancia ArgumentNullException con il nome del parametro compilato automaticamente quando customer è null, e altrimenti non fa nulla. Le versioni successive hanno aggiunto metodi simili come ArgumentException.ThrowIfNullOrEmpty (.NET 7) e ArgumentOutOfRangeException.ThrowIfNegative (.NET 8).

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA