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 InvalidOperationException
oggetto . 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.
- 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.
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:
- Procedura: Creare eccezioni definite dall'utente con messaggi di eccezione localizzati
- Risorse nelle app .NET
- System.Resources.ResourceManager
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):
- Usare ExceptionDispatchInfo.Capture(Exception) per acquisire un'eccezione e uno stack di chiamate.
- Usare ExceptionDispatchInfo.Throw() per ripristinare lo stato salvato quando l'eccezione è stata acquisita e rethrow l'eccezione acquisita.
- Utilizzare la proprietà per controllare l'eccezione ExceptionDispatchInfo.SourceException acquisita.
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
Commenti e suggerimenti
Invia e visualizza il feedback per