Procedure consigliate per le eccezioni
La gestione corretta delle eccezioni è essenziale per l'affidabilità dell'applicazione. È possibile gestire intenzionalmente le eccezioni previste per evitare che l'app si arresti in modo anomalo. Tuttavia, un'app arrestata in modo anomalo è più affidabile e diagnosticabile di un'app con un comportamento non definito.
In questo articolo vengono descritte le procedure consigliate per la gestione e la creazione di eccezioni.
Gestione delle eccezioni
Le procedure consigliate seguenti riguardano la gestione delle eccezioni:
- Usare i blocchi try/catch/finally per recuperare gli errori o rilasciare risorse
- Gestire le condizioni comuni per evitare le eccezioni
- Annullamento catch ed eccezioni asincrone
- Progettare le classi in modo da evitare le eccezioni
- Ripristinare lo stato quando i metodi non vengono completati a causa di eccezioni
- Acquisire e rigenerare correttamente le eccezioni
Usare blocchi try/catch/finally per correggere errori o rilasciare risorse
Per il codice che può potenzialmente generare un'eccezione e quando l'app può recuperare tale eccezione, usare try
/catch
blocchi intorno al codice. Nei blocchi catch
ordinare sempre le eccezioni dalla più derivata alla meno derivata. Tutte le eccezioni derivano dalla classe Exception. Altre eccezioni derivate non vengono gestite da una clausola catch
preceduta da una clausola catch
per una classe di eccezioni di base). Quando il codice non corregge un'eccezione, non recuperare l'eccezione. Abilitare i metodi nella parte superiore dello stack di chiamata per eseguire il ripristino, se possibile.
Pulire le risorse che sono allocate con istruzioni using
o blocchi finally
. Preferire le istruzioni using
per pulire automaticamente le risorse quando vengono generate eccezioni. Usare i blocchi finally
per pulire le risorse che non implementano IDisposable. Il codice in una clausola finally
viene quasi sempre eseguito anche se vengono generate eccezioni.
Gestire le condizioni comuni per evitare le eccezioni
Per le condizioni che potrebbero verificarsi, ma che potrebbero attivare un'eccezione, valutare la possibilità di gestirle in modo da evitare l'eccezione. Ad esempio, se si tenta di chiudere una connessione già chiusa, si otterrà InvalidOperationException
. Per impedire che ciò accada, usare un'istruzione if
per verificare lo stato della connessione prima di tentare di chiuderla.
if (conn.State != ConnectionState.Closed)
{
conn.Close();
}
If conn.State <> ConnectionState.Closed Then
conn.Close()
End IF
Se prima della chiusura non viene verificato lo stato della connessione, è possibile rilevare l'eccezione InvalidOperationException
.
try
{
conn.Close();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.GetType().FullName);
Console.WriteLine(ex.Message);
}
Try
conn.Close()
Catch ex As InvalidOperationException
Console.WriteLine(ex.GetType().FullName)
Console.WriteLine(ex.Message)
End Try
L'approccio da scegliere dipende dalla frequenza con cui si prevede che l'evento si verifichi.
Usare la gestione delle eccezioni se l'evento non si verifica spesso, ovvero, se l'evento è davvero eccezionale e indica un errore quale la fine imprevista di un file. Quando si utilizza la gestione delle eccezioni, una minore quantità di codice viene eseguita in condizioni normali.
Ricercare le condizioni di errore nel codice se l'evento si verifica ripetutamente e può essere considerato parte dell'esecuzione normale. Quando si ricercano le condizioni di errore comuni, viene eseguita una minore quantità di codice poiché vengono evitate le eccezioni.
Nota
I controlli iniziali eliminano la maggior parte delle eccezioni. Tuttavia, possono verificarsi race condition in cui la condizione controllata cambia tra il controllo e l'operazione. In tal caso, si può comunque incorrere in un'eccezione.
Chiamare i metodi Try*
per evitare le eccezioni
Se il costo delle prestazioni delle eccezioni è proibitivo, alcuni metodi della libreria .NET forniscono forme alternative di gestione degli errori. Ad esempio, Int32.Parse genera OverflowException se il valore da analizzare è troppo grande per essere rappresentato da Int32. Tuttavia, Int32.TryParse non genera questa eccezione. Restituisce invece un booleano e ha un parametro out
che contiene l'intero valido analizzato in caso di esito positivo. Dictionary<TKey,TValue>.TryGetValue ha un comportamento simile quando si tenta di ottenere un valore da un dizionario.
Annullamento catch ed eccezioni asincrone
Quando si chiama un metodo asincrono è preferibile intercettare OperationCanceledException anziché TaskCanceledException, che deriva da OperationCanceledException
. Molti metodi asincroni generano un'eccezione OperationCanceledException se viene richiesta l'annullamento. Queste eccezioni consentono di interrompere in modo efficiente l'esecuzione e di sbloccare il callstack dopo che viene osservata una richiesta di annullamento.
I metodi asincroni archiviano le eccezioni generate durante l'esecuzione nell'attività che restituiscono. Se un'eccezione viene archiviata nell'attività restituita, tale eccezione verrà generata quando l'attività è attesa. Le eccezioni di utilizzo, ad esempio ArgumentException, vengono comunque generate in modo sincrono. Per altre informazioni, vedere Eccezioni asincrone.
Progettare le classi in modo da evitare le eccezioni
Una classe può offrire metodi o proprietà che consentono di evitare di effettuare una chiamata che potrebbe generare un'eccezione. Ad esempio, la classe FileStream fornisce metodi che consentono di determinare se è stata raggiunta la fine del file. È possibile chiamare questi metodi per evitare l'eccezione che viene generata se si legge oltre la fine del file. L'esempio seguente illustra come continuare la lettura fino alla fine di un file senza generare un'eccezione:
class FileRead
{
public static void ReadAll(FileStream fileToRead)
{
ArgumentNullException.ThrowIfNull(fileToRead);
int b;
// Set the stream position to the beginning of the file.
fileToRead.Seek(0, SeekOrigin.Begin);
// Read each byte to the end of the file.
for (int i = 0; i < fileToRead.Length; i++)
{
b = fileToRead.ReadByte();
Console.Write(b.ToString());
// Or do something else with the byte.
}
}
}
Class FileRead
Public Sub ReadAll(fileToRead As FileStream)
' This if statement is optional
' as it is very unlikely that
' the stream would ever be null.
If fileToRead Is Nothing Then
Throw New System.ArgumentNullException()
End If
Dim b As Integer
' Set the stream position to the beginning of the file.
fileToRead.Seek(0, SeekOrigin.Begin)
' Read each byte to the end of the file.
For i As Integer = 0 To fileToRead.Length - 1
b = fileToRead.ReadByte()
Console.Write(b.ToString())
' Or do something else with the byte.
Next i
End Sub
End Class
Un altro modo per evitare le eccezioni consiste nel restituire null
(o impostazione predefinita) per i casi di errore più comuni, invece di generare un'eccezione. Un caso di errore comune può essere considerato come un normale flusso di controllo. Restituendo null
(o impostazione predefinita) in questi casi, si riduce al minimo l'impatto sulle prestazioni di un'app.
Per i tipi di valore, valutare se usare Nullable<T>
o default
come indicatore di errore per l'app. Usando Nullable<Guid>
, default
diventa null
invece di Guid.Empty
. Talvolta, l'aggiunta di Nullable<T>
può indicare più chiaramente quando un valore è presente o assente. Altre volte, l'aggiunta di Nullable<T>
può creare casi aggiuntivi da controllare che non sono necessari e serve solo per creare potenziali fonti di errore.
Ripristinare lo stato quando i metodi non sono completi a causa di eccezioni
I chiamanti dovrebbero avere la garanzia che non si verifichino effetti secondari quando un'eccezione viene generata da un metodo. Ad esempio, se è presente un codice che trasferisce denaro prelevandolo da un conto e depositandolo in un altro conto e viene generata un'eccezione durante l'esecuzione del deposito, non si desidera che il prelievo rimanga attivo.
public void TransferFunds(Account from, Account to, decimal amount)
{
from.Withdrawal(amount);
// If the deposit fails, the withdrawal shouldn't remain in effect.
to.Deposit(amount);
}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
from.Withdrawal(amount)
' If the deposit fails, the withdrawal shouldn't remain in effect.
[to].Deposit(amount)
End Sub
Il metodo precedente non genera direttamente eccezioni. Tuttavia, è necessario scrivere il metodo in modo che il ritiro venga invertito se l'operazione di deposito non riesce.
Un modo per gestire questa situazione consiste nel rilevare eventuali eccezioni generate dalla transazione di deposito e annullare il prelievo.
private static void TransferFunds(Account from, Account to, decimal amount)
{
string withdrawalTrxID = from.Withdrawal(amount);
try
{
to.Deposit(amount);
}
catch
{
from.RollbackTransaction(withdrawalTrxID);
throw;
}
}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
Dim withdrawalTrxID As String = from.Withdrawal(amount)
Try
[to].Deposit(amount)
Catch
from.RollbackTransaction(withdrawalTrxID)
Throw
End Try
End Sub
Questo esempio illustra l'uso di throw
per generare nuovamente l'eccezione originale rendendo in questo modo più semplice per i chiamanti visualizzare la causa effettiva del problema senza dover esaminare la proprietà InnerException. In alternativa è possibile generare una nuova eccezione e includere l'eccezione originale come eccezione interna.
catch (Exception ex)
{
from.RollbackTransaction(withdrawalTrxID);
throw new TransferFundsException("Withdrawal failed.", innerException: ex)
{
From = from,
To = to,
Amount = amount
};
}
Catch ex As Exception
from.RollbackTransaction(withdrawalTrxID)
Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With
{
.From = from,
.[To] = [to],
.Amount = amount
}
End Try
Acquisire e rigenerare correttamente le eccezioni
Una volta generata un'eccezione, parte delle informazioni che contiene è l'analisi dello stack. L'analisi dello stack è un elenco della gerarchia di chiamate al metodo che inizia con il metodo che genera l'eccezione e termina con il metodo che intercetta l'eccezione. Se si rigenera un'eccezione specificando l'eccezione nell'istruzione throw
, ad esempio throw e
, l'analisi dello stack viene riavviata nel metodo corrente e l'elenco delle chiamate a un metodo tra il metodo originale che ha lanciato l'eccezione e il metodo corrente viene perso. Per mantenere le informazioni sull'analisi dello stack originali con l'eccezione, esistono due opzioni che dipendono dalla posizione in cui si rigenera l'eccezione:
- Se si rigenera l'eccezione dall'interno del gestore ( blocco
catch
) che ha intercettato l'istanza dell'eccezione, usare l'istruzionethrow
senza specificare l'eccezione. La regola di analisi del codice CA2200 consente di trovare posizioni nel codice in cui è possibile perdere accidentalmente le informazioni di analisi dello stack. - Se si sta rigenerando l'eccezione da un punto diverso dal gestore ( blocco
catch
), usare ExceptionDispatchInfo.Capture(Exception) per acquisire l'eccezione nel gestore e ExceptionDispatchInfo.Throw() quando si vuole rigenerarla. È possibile usare la proprietà ExceptionDispatchInfo.SourceException per esaminare l'eccezione acquisita.
Nell'esempio seguente viene illustrato come usare la classe ExceptionDispatchInfo e l'aspetto dell'output.
ExceptionDispatchInfo? edi = null;
try
{
var txt = File.ReadAllText(@"C:\temp\file.txt");
}
catch (FileNotFoundException e)
{
edi = ExceptionDispatchInfo.Capture(e);
}
// ...
Console.WriteLine("I was here.");
if (edi is not null)
edi.Throw();
Se il file nel codice di esempio non esiste, viene generato l'output seguente:
I was here.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.
File name: 'C:\temp\file.txt'
at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
at System.IO.File.ReadAllText(String path, Encoding encoding)
at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12
--- End of stack trace from previous location ---
at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24
Generazione di eccezioni
Le procedure consigliate seguenti riguardano il modo in cui vengono generate le eccezioni:
- Usare tipi di eccezione predefiniti
- Usare i metodi del generatore di eccezioni
- Includere un messaggio di stringa localizzato
- Usare una grammatica corretta
- Posizionare le istruzioni throw
- Non generare eccezioni nelle clausole finally
- Non generare eccezioni da posizioni impreviste
- Generare eccezioni di convalida degli argomenti in modo sincrono
Usare tipi di eccezione predefiniti
Introdurre una nuova classe di eccezioni solo quando non è possibile applicare una classe predefinita. Ad esempio:
- Se una chiamata a un set di proprietà o a un metodo non è appropriata in base allo stato corrente dell'oggetto, generare un'eccezione InvalidOperationException.
- Se vengono passati parametri non validi, generare un'eccezione ArgumentException o una delle classi predefinite che derivano da ArgumentException.
Nota
Sebbene sia consigliabile usare tipi di eccezione predefiniti quando possibile, non è consigliabile generare alcuni tipi di eccezione riservati, ad esempio AccessViolationException, IndexOutOfRangeException, NullReferenceException e StackOverflowException. Per altre informazioni, vedere CA2201: Non generare tipi di eccezione riservati.
Usare metodi per la creazione di eccezioni
Una classe genera spesso la stessa eccezione da punti diversi dell'implementazione. Per evitare un eccesso di codice, creare un metodo helper che crei l'eccezione e la restituisca. Ad esempio:
class FileReader
{
private readonly string _fileName;
public FileReader(string path)
{
_fileName = path;
}
public byte[] Read(int bytes)
{
byte[] results = FileUtils.ReadFromFile(_fileName, bytes) ?? throw NewFileIOException();
return results;
}
static FileReaderException NewFileIOException()
{
string description = "My NewFileIOException Description";
return new FileReaderException(description);
}
}
Class FileReader
Private fileName As String
Public Sub New(path As String)
fileName = path
End Sub
Public Function Read(bytes As Integer) As Byte()
Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes)
If results Is Nothing
Throw NewFileIOException()
End If
Return results
End Function
Function NewFileIOException() As FileReaderException
Dim description As String = "My NewFileIOException Description"
Return New FileReaderException(description)
End Function
End Class
Alcuni tipi di eccezione .NET chiave hanno metodi helper statici throw
che allocano e generano l'eccezione. È consigliabile chiamare questi metodi invece di costruire e generare il tipo di eccezione corrispondente:
- ArgumentNullException.ThrowIfNull
- ArgumentException.ThrowIfNullOrEmpty(String, String)
- ArgumentException.ThrowIfNullOrWhiteSpace(String, String)
- ArgumentOutOfRangeException.ThrowIfZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfNegative<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNotEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThanOrEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<T>(T, T, String)
- ObjectDisposedException.ThrowIf
Suggerimento
Le regole di analisi del codice seguenti consentono di trovare posizioni nel codice in cui è possibile sfruttare questi helper statici throw
: CA1510, CA1511, CA1512e CA1513.
Se si implementa un metodo asincrono, chiamare CancellationToken.ThrowIfCancellationRequested() invece di verificare se è stato richiesto l'annullamento e quindi costruire e generare OperationCanceledException. Per altre informazioni, vedere CA2250.
Includere un messaggio di stringa localizzato
Il messaggio di errore visualizzato all'utente è derivato dalla proprietà Exception.Message dell'eccezione generata e non dal nome della classe di eccezione. In genere, si assegna un valore alla proprietà Exception.Message passando la stringa del messaggio all'argomento message
di un costruttore di eccezione.
Per le applicazioni localizzate, è necessario specificare una stringa di messaggio localizzata per ogni eccezione che può essere generata dall'applicazione. Usare i file di risorse per specificare i messaggi di errore localizzati. Per informazioni sulla localizzazione delle applicazioni e sul recupero di stringhe localizzate, vedere gli articoli seguenti:
- Procedura: Creare eccezioni definite dall'utente con messaggi di eccezione localizzati
- Risorse nelle app .NET
- System.Resources.ResourceManager
Usare la grammatica corretta
Scrivere frasi chiare e includere la punteggiatura finale. Ogni frase della stringa assegnata alla proprietà Exception.Message deve terminare con un punto. Ad esempio, "La tabella di log ha superato l'overflow". Usa la grammatica e la punteggiatura corrette.
Posizionare correttamente le istruzioni throw
Posizionare le istruzioni throw in cui è utile l'analisi dello stack. La traccia dello stack inizia in corrispondenza dell'istruzione in cui l'eccezione viene generata e termina in corrispondenza dell'istruzione catch
che intercetta l'eccezione.
Non generare eccezioni nelle clausole finally
Non generare eccezioni nelle clausole finally
. Per altre informazioni, vedere la regola di analisi del codice CA2219.
Non generare eccezioni da posizioni impreviste
Alcuni metodi, ad esempio Equals
, GetHashCode
e i metodi ToString
, i costruttori statici e gli operatori di uguaglianza, non devono generare eccezioni. Per altre informazioni, vedere la regola di analisi del codice CA1065.
Generare eccezioni di convalida degli argomenti in modo sincrono
Nei metodi che restituiscono attività, è necessario convalidare gli argomenti e generare le eccezioni corrispondenti, come ArgumentException e ArgumentNullException, prima di immettere la parte asincrona del metodo. Le eccezioni generate nella parte asincrona del metodo vengono archiviate nell'attività restituita e non emergono finché, ad esempio, l'attività non è attesa. Per altre informazioni, vedere Eccezioni nei metodi che restituiscono attività.
Tipi di eccezione personalizzati
Le procedure consigliate seguenti riguardano i tipi di eccezione personalizzati:
- Terminare i nomi delle classi di eccezioni con
Exception
- Includere tre costruttori
- Fornire proprietà aggiuntive in base alle esigenze
Terminare i nomi delle classi di eccezioni con la parola Exception
Quando è necessaria un'eccezione personalizzata, assegnare un nome appropriato all'eccezione e derivarla dalla classe Exception. Ad esempio:
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
Inherits Exception
End Class
Includere tre costruttori
Usare almeno i tre costruttori comuni quando si creano classi di eccezione personalizzate: il costruttore senza parametri, un costruttore che accetta un messaggio stringa e un costruttore che accetta un messaggio stringa e un'eccezione interna.
- Exception(), che usa valori predefiniti.
- Exception(String), che accetta un messaggio stringa.
- Exception(String, Exception), che accetta un messaggio stringa e un'eccezione interna.
Per un esempio, vedere Procedura: Creare eccezioni definite dall'utente.
Fornire ulteriori proprietà se necessario
Specificare le proprietà aggiuntive di un'eccezione (oltre alla stringa del messaggio personalizzata) solo in un contesto di codice in cui è utile avere a disposizione altre informazioni. Ad esempio, FileNotFoundException fornisce la proprietà FileName.