Procedure consigliate per le eccezioni

Un'applicazione progettata correttamente gestisce eccezioni ed errori per impedire arresti anomali dell'applicazione. In questo articolo vengono descritte le procedure consigliate per la gestione e la creazione di eccezioni.

Usare blocchi try/catch/finally per correggere errori o rilasciare risorse

Usare try/catch blocchi intorno al codice che può potenzialmente generare un'eccezione e il codice può essere ripristinato da tale eccezione. Nei blocchi catch ordinare sempre le eccezioni dalla più derivata alla meno derivata. Tutte le eccezioni derivano dalla Exception classe . 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 può essere ripristinato da un'eccezione, non rilevare tale eccezione. Abilitare i metodi nella parte superiore dello stack di chiamata per eseguire il ripristino, se possibile.

Pulire le risorse allocate con using istruzioni o finally blocchi. 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 senza generare eccezioni

È consigliabile gestire le condizioni che potrebbero verificarsi ma potrebbero generare un'eccezione in modo da evitare l'eccezione. Ad esempio, se si tenta di chiudere una connessione già chiusa, si otterrà un InvalidOperationExceptionoggetto . 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)
{
    conn.Close();
}
If conn.State <> ConnectionState.Closed Then
    conn.Close()
End IF

Se non si controlla lo stato della connessione prima della chiusura, è possibile rilevare l'eccezione InvalidOperationException .

try
{
    conn->Close();
}
catch (InvalidOperationException^ ex)
{
    Console::WriteLine(ex->GetType()->FullName);
    Console::WriteLine(ex->Message);
}
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

Nella scelta del metodo è necessario considerare la frequenza con cui si prevede che l'evento possa verificarsi.

  • Usare la gestione delle eccezioni se l'evento non si verifica spesso, ovvero se l'evento è veramente eccezionale e indica un errore, ad esempio un end-of-file imprevisto. 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.

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. Con la classe FileStream, ad esempio, sono disponibili metodi che consentono di determinare se è stata raggiunta la fine del file. Questi metodi possono essere usati per evitare l'eccezione generata se si legge la fine del file. Nell'esempio seguente viene illustrato come leggere alla fine di un file senza attivare un'eccezione:

class FileRead
{
public:
    void ReadAll(FileStream^ fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == nullptr)
        {
            throw gcnew System::ArgumentNullException();
        }

        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 void ReadAll(FileStream fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == null)
        {
            throw new ArgumentNullException();
        }

        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 default) in questi casi, si riduce al minimo l'impatto sulle prestazioni di un'app.

Per i tipi di valore, se usare Nullable<T> o predefinito come indicatore di errore è qualcosa da considerare per l'app. Usando Nullable<Guid>, default diventa null invece di Guid.Empty. A volte, l'aggiunta Nullable<T> può renderla più chiara quando un valore è presente o assente. Altre volte, l'aggiunta Nullable<T> può creare casi aggiuntivi per verificare che non siano necessari e solo per creare potenziali origini di errori.

Generare eccezioni anziché restituire un codice di errore

Le eccezioni assicurano che gli errori non vengano rilevati perché il codice chiamante non ha controllato un codice restituito.

Usare i tipi di eccezione .NET predefiniti

Introdurre una nuova classe di eccezioni solo quando non è possibile applicare una classe predefinita. Ad esempio:

  • Se una chiamata al set di proprietà o al 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.

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 ref class MyFileNotFoundException : public Exception
{
};
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
    Inherits Exception
End Class

Inserire tre costruttori nelle classi di eccezioni personalizzate

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.

Garantire la disponibilità dei dati dell'eccezione per il codice eseguito in modalità remota

Quando si creano eccezioni definite dall'utente, assicurarsi che i metadati per le eccezioni siano disponibili per il codice in esecuzione in remoto.

Ad esempio, nelle implementazioni di .NET che supportano i domini dell'app, le eccezioni potrebbero verificarsi tra domini app. Si supponga che il dominio dell'app A crei il dominio dell'app B, che esegue il codice che genera un'eccezione. Per il dominio dell'app A per rilevare correttamente e gestire l'eccezione, deve essere in grado di trovare l'assembly contenente l'eccezione generata dal dominio dell'app B. Se il dominio dell'app B genera un'eccezione contenuta in un assembly sotto la relativa base dell'applicazione, ma non nella base dell'applicazione del dominio app A, il dominio dell'app A non sarà in grado di trovare l'eccezione e Common Language Runtime genererà un'eccezione FileNotFoundException . Per evitare questa situazione, è possibile distribuire l'assembly contenente le informazioni sull'eccezione in uno dei due modi seguenti:

  • Inserendo l'assembly in una base applicativa comune condivisa da entrambi i domini applicazione
  • Se i domini non condividono una base di applicazioni comune, firmare l'assembly contenente le informazioni sull'eccezione con un nome sicuro e distribuire l'assembly nella global assembly cache.

Usare messaggi di errore grammaticalmente corretti

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 sovraflow". È una stringa di messaggio appropriata.

Includere in ogni eccezione un messaggio stringa localizzato

Il messaggio di errore visualizzato dall'utente viene derivato dalla Exception.Message proprietà dell'eccezione generata e non dal nome della classe di eccezione. In genere, si assegna un valore alla Exception.Message proprietà passando la stringa di messaggio all'argomento message di un costruttore Exception.

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:

Nelle eccezioni personalizzate specificare le proprietà aggiuntive necessarie

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.

Inserire istruzioni throw in modo che l'analisi dello stack risulti utile

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.

Usare metodi per la creazione di eccezioni

È comune che una classe generi la stessa eccezione da posizioni diverse nell'implementazione. Per evitare codice di dimensioni eccessive, utilizzare metodi di supporto che creano l'eccezione e la restituiscono. Ad esempio:

ref class FileReader
{
private:
    String^ fileName;

public:
    FileReader(String^ path)
    {
        fileName = path;
    }

    array<Byte>^ Read(int bytes)
    {
        array<Byte>^ results = FileUtils::ReadFromFile(fileName, bytes);
        if (results == nullptr)
        {
            throw NewFileIOException();
        }
        return results;
    }

    FileReaderException^ NewFileIOException()
    {
        String^ description = "My NewFileIOException Description";

        return gcnew FileReaderException(description);
    }
};
class FileReader
{
    private string fileName;

    public FileReader(string path)
    {
        fileName = path;
    }

    public byte[] Read(int bytes)
    {
        byte[] results = FileUtils.ReadFromFile(fileName, bytes);
        if (results == null)
        {
            throw NewFileIOException();
        }
        return results;
    }

    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

In alcuni casi è preferibile usare il costruttore dell'eccezione per compilare l'eccezione. Un esempio è una classe di eccezioni globali, ad esempio ArgumentException.

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 prelievo venga invertito se l'operazione di deposito ha esito negativo.

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

In questo esempio viene illustrato l'uso di throw per ripristinare l'eccezione originale, rendendo più semplice per i chiamanti visualizzare la causa reale del problema senza dover esaminare la InnerException proprietà. 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 eccezioni a rethrow in un secondo momento

Per acquisire un'eccezione e conservare il callstack in modo da poterlo riprovare in un secondo momento, usare la System.Runtime.ExceptionServices.ExceptionDispatchInfo classe . Questa classe fornisce i metodi e le proprietà seguenti (tra gli altri):

Nell'esempio seguente viene illustrato il modo in cui è possibile usare la classe e come potrebbe essere simile all'output ExceptionDispatchInfo .

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

Vedi anche