Condividi tramite


Procedure consigliate per le eccezioni

La gestione corretta delle eccezioni è essenziale per l'affidabilità dell'applicazione. Puoi 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 rispetto a un'app con un comportamento indefinito.

Questo articolo descrive 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 eseguire il ripristino da errori o rilasciare risorse

Per il codice che può potenzialmente generare un'eccezione, e quando l'app può eseguire il ripristino da tale eccezione, usare blocchi try/catch 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 eccezione di base. Quando il codice non riesce a eseguire il ripristino da un'eccezione, non intercettare l'eccezione. Abilitare i metodi più avanti nello stack di chiamate per il ripristino, se possibile.

Pulire le risorse 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 quando vengono generate eccezioni.

Gestire le condizioni comuni per evitare eccezioni

Per le condizioni che potrebbero verificarsi, ma 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à un InvalidOperationException. È possibile evitare che usando un'istruzione if per controllare 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 non si controlla lo stato della connessione prima della chiusura, è possibile intercettare 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 si verifichi l'evento.

  • Usare la gestione delle eccezioni se l'evento non si verifica spesso, ovvero se l'evento è veramente eccezionale e indica un errore, ad esempio una fine del file imprevista. Quando si usa la gestione delle eccezioni, meno codice viene eseguito in condizioni normali.

  • Verificare la presenza di condizioni di errore nel codice se l'evento si verifica regolarmente e può essere considerato parte della normale esecuzione. Quando si verifica la presenza di condizioni di errore comuni, viene eseguita una minore quantità di codice perché si evitano eccezioni.

    Nota

    I controlli iniziali eliminano la maggior parte delle eccezioni. Tuttavia, possono verificarsi condizioni di gara in cui la condizione sorvegliata cambia tra il controllo e l'operazione e, in tal caso, è possibile incontrare comunque un'eccezione.

Chiamare i metodi Try* per evitare 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 un OverflowException se il valore da analizzare è troppo grande per essere rappresentato da Int32. Tuttavia, Int32.TryParse non genera questa eccezione. Restituisce invece un valore Boolean e ha un parametro out che contiene l'intero valido analizzato in caso di esito positivo. Dictionary<TKey,TValue>.TryGetValue ha un comportamento simile per tentare di ottenere un valore da un dizionario.

Intercettare l'annullamento e le eccezioni asincrone

È preferibile intercettare OperationCanceledException anziché TaskCanceledException, che deriva da OperationCanceledException, quando si chiama un metodo asincrono. Molti metodi asincroni generano un'eccezione OperationCanceledException se viene richiesto l'annullamento. Queste eccezioni consentono l'interruzione efficiente dell'esecuzione e lo stack delle chiamate viene smontato una volta 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 ulteriori informazioni, vedere eccezioni asincrone.

Progettare classi in modo che le eccezioni possano essere evitate

Una classe può fornire metodi o proprietà che consentono di evitare di effettuare una chiamata che attiverebbe un'eccezione. Ad esempio, la classe FileStream fornisce metodi che consentono di determinare se la fine del file è stata raggiunta. È possibile chiamare questi metodi per evitare l'eccezione generata se si legge oltre la fine del file. L'esempio seguente illustra come leggere fino alla fine di un file senza attivare 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 eccezioni consiste nel restituire null (o impostazione predefinita) per i casi di errore più comuni anziché generare un'eccezione. Un caso di errore comune può essere considerato 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 utilizzare Nullable<T> o default come indicatore di errore per la propria applicazione. Usando Nullable<Guid>, default diventa null anziché Guid.Empty. In alcuni casi, l'aggiunta di Nullable<T> può rendere più chiaro quando un valore è presente o assente. In altri casi, l'aggiunta di Nullable<T> può creare casi aggiuntivi per verificare che non siano necessari e servire solo per creare potenziali fonti di errori.

Ripristinare lo stato quando i metodi non vengono completati a causa di eccezioni

I chiamanti devono essere in grado di presupporre che non ci siano effetti collaterali quando viene generata un'eccezione da un metodo. Ad esempio, se hai codice che trasferisce denaro ritirando da un conto e depositando in un altro conto e viene generata un'eccezione durante l'esecuzione del deposito, non vuoi 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 è 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

In questo esempio viene illustrato l'uso di throw per rigenerare l'eccezione originale, rendendo più semplice per i chiamanti visualizzare la causa reale del problema senza dover esaminare la proprietà InnerException. Un'alternativa consiste nel 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 è la traccia 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 di chiamate al metodo tra il metodo originale che ha generato l'eccezione e il metodo corrente viene perso. Per mantenere le informazioni della traccia dello stack originali con l'eccezione, esistono due opzioni che dipendono dalla posizione in cui si sta rilanciando l'eccezione.

  • Se si rigenera l'eccezione dall'interno del gestore che ha intercettato l'istanza dell'eccezione (bloccocatch), 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 rilanciando l'eccezione da un punto diverso dal gestore (il bloccocatch), utilizzare ExceptionDispatchInfo.Capture(Exception) per acquisire l'eccezione nel gestore e ExceptionDispatchInfo.Throw() quando si vuole rilanciarla. È possibile utilizzare 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

Lancio di eccezioni

Le procedure consigliate seguenti riguardano il modo in cui vengono generate eccezioni:

Usare tipi di eccezione predefiniti

Introdurre una nuova classe di eccezione solo quando non è applicabile una classe predefinita. Per 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 i metodi del generatore di eccezioni

È comune che una classe generi la stessa eccezione da posizioni diverse nell'implementazione. Per evitare un numero eccessivo di codice, creare un metodo helper che crea l'eccezione e lo restituisce. Per 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 importanti tipi di eccezione .NET dispongono di metodi statici di helper throw che allocano ed espongono 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 implementate un metodo asincrono, chiamate CancellationToken.ThrowIfCancellationRequested() invece di verificare se è stato richiesto l'annullamento e quindi costruire e lanciare OperationCanceledException. Per altre informazioni, vedere CA2250.

Includere un messaggio di stringa localizzato

Il messaggio di errore visualizzato dall'utente deriva 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 exception .

Per le applicazioni localizzate, è necessario fornire una stringa di messaggio localizzata per ogni eccezione che l'applicazione può generare. I file di risorse vengono usati per fornire messaggi di errore localizzati. Per informazioni sulla localizzazione delle applicazioni e sul recupero di stringhe localizzate, vedere gli articoli seguenti:

Usare la grammatica appropriata

Scrivere frasi chiare e includere la punteggiatura finale. Ogni frase nella stringa assegnata alla proprietà Exception.Message deve terminare con il punto. Ad esempio, "La tabella di log ha tracimato." Usa la grammatica e la punteggiatura corrette.

Posizionare correttamente le istruzioni throw

Posizionare istruzioni throw nei punti in cui il traceback dello stack sarà utile. L'analisi dello stack inizia con l'istruzione in cui viene generata l'eccezione e termina con l'istruzione catch che intercetta l'eccezione.

Non generare eccezioni nelle clausole finally

Non sollevare eccezioni nelle clausole finally. Per altre informazioni, vedere Regola di analisi del codice CA2219.

Non generare eccezioni da posizioni impreviste

Alcuni metodi, ad esempio Equals, GetHashCodee ToString metodi, costruttori statici e operatori di uguaglianza, non devono generare eccezioni. Per altre informazioni, vedere 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 eventuali eccezioni corrispondenti, ad esempio 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 fino a quando, ad esempio, l'attività non viene attesa. Per maggiori informazioni, vedere Eccezioni nei metodi che restituiscono attività .

Tipi di eccezione personalizzati

Le procedure consigliate seguenti riguardano i tipi di eccezione personalizzati:

Termina i nomi delle classi di eccezione con Exception

Quando è necessaria un'eccezione personalizzata, denominarla in modo appropriato e derivarla dalla classe Exception. Per 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.

Specificare proprietà aggiuntive in base alle esigenze

Specificare proprietà aggiuntive per un'eccezione (oltre alla stringa di messaggio personalizzata) solo quando è presente uno scenario programmatico in cui sono utili le informazioni aggiuntive. Ad esempio, il FileNotFoundException fornisce la proprietà FileName.

Vedere anche

  • Linee guida di progettazione per eccezioni