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 i blocchi try/catch intorno al codice che può potenzialmente generare un'eccezione e che il codice non può correggere. Nei blocchi catch ordinare sempre le eccezioni dalla più derivata alla meno derivata. Tutte le eccezioni derivano dalla classe Exception. Le eccezioni più derivate non vengono gestite da una clausola catch preceduta da una clausola catch per una classe di eccezione 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 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à 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)
{
    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 (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 è 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.

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 che verrebbe generata se si continua la lettura 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:
    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 le eccezioni consiste nel restituire Null (o default) 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 default) in questi casi, si riduce al minimo l'impatto sulle prestazioni di un'app.

Per i tipi di valore, la scelta tra Nullable<T> o default come indicatore di errore è un aspetto da considerare in relazione all'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.

Generare eccezioni anziché restituire un codice di errore

Le eccezioni garantiscono il rilevamento degli errori quando il codice chiamante non rileva 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 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.

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, garantire che i metadati relativi alle eccezioni siano disponibili per il codice eseguito in modalità remota.

Ad esempio, nelle implementazioni .NET che supportano domini app, è possibile che si verifichino eccezioni nei domini app. Si supponga che il dominio app A crei il dominio app B che esegue codice che genera un'eccezione. Per rilevare e gestire correttamente l'eccezione è necessario che il dominio app A sia in grado di individuare l'assembly contenente l'eccezione generata dal dominio app B. Se il dominio app B genera un'eccezione contenuta in un assembly nella relativa base dell'applicazione ma non nella base dell'applicazione del dominio app A, il dominio app A non sarà in grado di individuare l'eccezione e Common Language Runtime genererà un'eccezione FileNotFoundException. Per evitare che questo si verifichi è possibile distribuire l'assembly contenente le informazioni sull'eccezione in due modi:

  • Inserendo l'assembly in una base applicativa comune condivisa da entrambi i domini applicazione
  • Se i domini non condividono alcuna base applicativa comune, firmando l'assembly contenente le informazioni sull'eccezione con un nome sicuro e distribuendo tale 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 superato l'overflow." è una stringa di messaggio appropriata.

Includere in ogni eccezione un messaggio 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:

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

Una classe genera spesso la stessa eccezione da punti diversi dell'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 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 le eccezioni da rigenerare in un secondo momento

Per acquisire un'eccezione e conservarne lo stack di chiamate per poterlo rigenerare in un secondo momento, usare la classe System.Runtime.ExceptionServices.ExceptionDispatchInfo. Questa classe fornisce i metodi e le proprietà seguenti (tra gli altri):

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

Vedi anche