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 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 ( bloccocatch ) che ha intercettato l'istanza dell'eccezione, usare l'istruzione throw 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 ( bloccocatch ), 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

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:

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:

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, GetHashCodee 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 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.

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.

Vedi anche