Práticas recomendadas para exceções
O tratamento adequado de exceções é essencial para a confiabilidade do aplicativo. Você pode lidar intencionalmente com as exceções esperadas para evitar que seu aplicativo falhe. No entanto, um aplicativo com falha é mais confiável e diagnosticável do que um aplicativo com comportamento indefinido.
Este artigo descreve as práticas recomendadas para lidar e criar exceções.
Processamento de exceções
As seguintes práticas recomendadas dizem respeito à forma como você lida com exceções:
- Use try/catch/finally blocks para recuperar de erros ou liberar recursos
- Lidar com condições comuns para evitar exceções
- Cancelamento de capturas e exceções assíncronas
- Classes de design para que exceções possam ser evitadas
- Restaurar o estado quando os métodos não são concluídos devido a exceções
- Capturar e relançar exceções corretamente
Use try/catch/finally blocks para recuperar de erros ou liberar recursos
Para código que pode gerar uma exceção e quando seu aplicativo puder se recuperar dessa exceção, use try
/catch
blocos ao redor do código. Em catch
blocos, sempre ordene exceções do mais derivado para o menos derivado. (Todas as exceções derivam da Exception classe. Mais exceções derivadas não são tratadas por uma catch
cláusula precedida por uma catch
cláusula para uma classe de exceção base.) Quando seu código não puder se recuperar de uma exceção, não detete essa exceção. Habilite métodos mais acima na pilha de chamadas para recuperar, se possível.
Limpe os recursos alocados com using
instruções ou finally
blocos. Prefira using
instruções para limpar recursos automaticamente quando exceções são lançadas. Use finally
blocos para limpar recursos que não implementam IDisposableo . O código em uma finally
cláusula é quase sempre executado, mesmo quando exceções são lançadas.
Lidar com condições comuns para evitar exceções
Para condições que provavelmente ocorrerão, mas que podem desencadear uma exceção, considere tratá-las de uma forma que evite a exceção. Por exemplo, se você tentar fechar uma conexão que já está fechada, obterá um InvalidOperationException
arquivo . Você pode evitar isso usando uma if
instrução para verificar o estado da conexão antes de tentar fechá-la.
if (conn.State != ConnectionState.Closed)
{
conn.Close();
}
If conn.State <> ConnectionState.Closed Then
conn.Close()
End IF
Se você não verificar o estado da conexão antes de fechar, poderá detetar a InvalidOperationException
exceção.
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
A abordagem a escolher depende da frequência com que espera que o evento ocorra.
Use o tratamento de exceções se o evento não ocorrer com frequência, ou seja, se o evento for realmente excecional e indicar um erro, como um fim de arquivo inesperado. Quando você usa o tratamento de exceções, menos código é executado em condições normais.
Verifique se há condições de erro no código se o evento acontece rotineiramente e pode ser considerado parte da execução normal. Quando você verifica condições de erro comuns, menos código é executado porque você evita exceções.
Nota
As verificações iniciais eliminam exceções na maioria das vezes. No entanto, pode haver condições de corrida em que a condição protegida muda entre a verificação e a operação e, nesse caso, você ainda pode incorrer em uma exceção.
Métodos de chamada Try*
para evitar exceções
Se o custo de desempenho das exceções for proibitivo, alguns métodos de biblioteca .NET fornecem formas alternativas de tratamento de erros. Por exemplo, Int32.Parse lança um OverflowException se o valor a ser analisado for muito grande para ser representado por Int32. No entanto, Int32.TryParse não lança esta exceção. Em vez disso, ele retorna um Boolean e tem um out
parâmetro que contém o inteiro válido analisado após o sucesso. Dictionary<TKey,TValue>.TryGetValue tem comportamento semelhante para tentar obter um valor de um dicionário.
Cancelamento de capturas e exceções assíncronas
É melhor capturar OperationCanceledException em vez de TaskCanceledException, que deriva de OperationCanceledException
, quando você chama um método assíncrono. Muitos métodos assíncronos lançam uma OperationCanceledException exceção se o cancelamento for solicitado. Essas exceções permitem que a execução seja interrompida de forma eficiente e que a pilha de chamadas seja desenrolada assim que uma solicitação de cancelamento for observada.
Os métodos assíncronos armazenam exceções que são lançadas durante a execução na tarefa que retornam. Se uma exceção for armazenada na tarefa retornada, essa exceção será lançada quando a tarefa for aguardada. As exceções de uso, como ArgumentException, ainda são lançadas de forma síncrona. Para obter mais informações, consulte Exceções assíncronas.
Classes de design para que exceções possam ser evitadas
Uma classe pode fornecer métodos ou propriedades que permitem evitar fazer uma chamada que acionaria uma exceção. Por exemplo, a FileStream classe fornece métodos que ajudam a determinar se o final do arquivo foi atingido. Você pode chamar esses métodos para evitar a exceção que é lançada se você ler após o final do arquivo. O exemplo a seguir mostra como ler até o final de um arquivo sem acionar uma exceção:
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
Outra maneira de evitar exceções é retornar null
(ou padrão) para os casos de erro mais comuns em vez de lançar uma exceção. Um caso de erro comum pode ser considerado um fluxo normal de controle. Ao retornar null
(ou padrão) nesses casos, você minimiza o impacto no desempenho de um aplicativo.
Para tipos de valor, considere se deseja usar Nullable<T>
ou default
como o indicador de erro para seu aplicativo. Ao usar Nullable<Guid>
, default
torna-se null
em vez de Guid.Empty
. Às vezes, a adição Nullable<T>
pode tornar mais claro quando um valor está presente ou ausente. Outras vezes, adicionar Nullable<T>
pode criar casos extras para verificar que não são necessários e servir apenas para criar potenciais fontes de erros.
Restaurar o estado quando os métodos não são concluídos devido a exceções
Os chamadores devem ser capazes de assumir que não há efeitos colaterais quando uma exceção é lançada de um método. Por exemplo, se você tiver um código que transfere dinheiro sacando de uma conta e depositando em outra conta, e uma exceção for lançada durante a execução do depósito, você não quer que o saque permaneça em vigor.
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
O método anterior não lança diretamente nenhuma exceção. No entanto, você deve escrever o método para que a retirada seja revertida se a operação de depósito falhar.
Uma maneira de lidar com essa situação é pegar quaisquer exceções lançadas pela transação de depósito e reverter o saque.
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
Este exemplo ilustra o uso de throw
para relançar a exceção original, tornando mais fácil para os chamadores verem a causa real do problema sem ter que examinar a InnerException propriedade. Uma alternativa é lançar uma nova exceção e incluir a exceção original como exceção 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
Capturar e relançar exceções corretamente
Uma vez que uma exceção é lançada, parte das informações que ela carrega é o rastreamento de pilha. O rastreamento de pilha é uma lista da hierarquia de chamada de método que começa com o método que lança a exceção e termina com o método que captura a exceção. Se você relançar uma exceção especificando a exceção na throw
instrução, por exemplo, throw e
, o rastreamento de pilha será reiniciado no método atual e a lista de chamadas de método entre o método original que lançou a exceção e o método atual será perdida. Para manter as informações de rastreamento de pilha originais com a exceção, há duas opções que dependem de onde você está lançando a exceção:
- Se você relançar a exceção de dentro do manipulador (
catch
block) que detetou a instância de exceção, use athrow
instrução sem especificar a exceção. A regra de análise de código CA2200 ajuda você a encontrar lugares em seu código onde você pode perder inadvertidamente informações de rastreamento de pilha. - Se você estiver relançando a exceção de algum lugar diferente do manipulador (
catch
bloco ), use ExceptionDispatchInfo.Capture(Exception) para capturar a exceção no manipulador e ExceptionDispatchInfo.Throw() quando quiser relançá-la. Você pode usar a ExceptionDispatchInfo.SourceException propriedade para inspecionar a exceção capturada.
O exemplo a seguir mostra como a ExceptionDispatchInfo classe pode ser usada e como a saída pode parecer.
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 o arquivo no código de exemplo não existir, a seguinte saída será produzida:
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
Abrindo exceções
As seguintes práticas recomendadas dizem respeito à forma como você lança exceções:
- Usar tipos de exceção predefinidos
- Usar métodos do construtor de exceções
- Incluir uma mensagem de cadeia de caracteres localizada
- Use a gramática adequada
- Coloque bem as declarações
- Não levante exceções em cláusulas finais
- Não levante exceções de lugares inesperados
- Lançar exceções de validação de argumento de forma síncrona
Usar tipos de exceção predefinidos
Introduza uma nova classe de exceção somente quando uma predefinida não se aplicar. Por exemplo:
- Se um conjunto de propriedades ou chamada de método não for apropriado dado o estado atual do objeto, lance uma InvalidOperationException exceção.
- Se parâmetros inválidos forem passados, lance uma ArgumentException exceção ou uma das classes predefinidas que derivam de ArgumentException.
Nota
Embora seja melhor usar tipos de exceção predefinidos quando possível, você não deve gerar alguns tipos de exceção reservados, como AccessViolationException, IndexOutOfRangeExceptionNullReferenceException e StackOverflowException. Para obter mais informações, consulte CA2201: Não gerar tipos de exceção reservados.
Usar métodos do construtor de exceções
É comum que uma classe lance a mesma exceção de lugares diferentes em sua implementação. Para evitar código excessivo, crie um método auxiliar que crie a exceção e a retorne. Por exemplo:
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
Alguns tipos de exceção .NET chave têm esses métodos auxiliares estáticos throw
que alocam e lançam a exceção. Você deve chamar esses métodos em vez de construir e lançar o tipo de exceção correspondente:
- ArgumentNullException.ThrowIfNull
- ArgumentException.ThrowIfNullOrEmpty(String, String)
- ArgumentException.ThrowIfNullOrWhiteSpace(String, String)
- ArgumentOutOfRangeException.ThrowIfZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfNegative<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNotEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThanOrEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<T>(T, T, String)
- ObjectDisposedException.ThrowIf
Gorjeta
As seguintes regras de análise de código podem ajudá-lo a encontrar locais em seu código onde você pode aproveitar esses auxiliares estáticos throw
: CA1510, CA1511, CA1512 e CA1513.
Se você estiver implementando um método assíncrono, ligue CancellationToken.ThrowIfCancellationRequested() em vez de verificar se o cancelamento foi solicitado e, em seguida, construa e lance OperationCanceledException. Para obter mais informações, consulte CA2250.
Incluir uma mensagem de cadeia de caracteres localizada
A mensagem de erro que o usuário vê é derivada Exception.Message da propriedade da exceção que foi lançada, e não do nome da classe de exceção. Normalmente, você atribui um valor à Exception.Message propriedade passando a cadeia de caracteres de mensagem para o message
argumento de um construtor Exception.
Para aplicativos localizados, você deve fornecer uma cadeia de caracteres de mensagem localizada para cada exceção que seu aplicativo pode lançar. Você usa arquivos de recursos para fornecer mensagens de erro localizadas. Para obter informações sobre como localizar aplicativos e recuperar cadeias de caracteres localizadas, consulte os seguintes artigos:
- Como: Criar exceções definidas pelo usuário com mensagens de exceção localizadas
- Recursos em aplicativos .NET
- System.Resources.ResourceManager
Use a gramática adequada
Escreva frases claras e inclua pontuação final. Cada frase na cadeia de caracteres atribuída à Exception.Message propriedade deve terminar em um ponto. Por exemplo, "A tabela de log estourou." usa gramática e pontuação corretas.
Coloque bem as declarações
Coloque instruções throw onde o rastreamento de pilha será útil. O rastreamento de pilha começa na instrução onde a exceção é lançada e termina na catch
instrução que captura a exceção.
Não levante exceções em cláusulas finais
Não levante exceções em finally
cláusulas. Para obter mais informações, consulte a regra de análise de código CA2219.
Não levante exceções de lugares inesperados
Alguns métodos, como Equals
, GetHashCode
e ToString
métodos, construtores estáticos e operadores de igualdade, não devem lançar exceções. Para obter mais informações, consulte a regra de análise de código CA1065.
Lançar exceções de validação de argumento de forma síncrona
Nos métodos de retorno de tarefas, você deve validar argumentos e lançar quaisquer exceções correspondentes, como ArgumentException e ArgumentNullException, antes de inserir a parte assíncrona do método. As exceções lançadas na parte assíncrona do método são armazenadas na tarefa retornada e não surgem até que, por exemplo, a tarefa seja aguardada. Para obter mais informações, consulte Exceções em métodos de retorno de tarefas.
Tipos de exceção personalizados
As seguintes práticas recomendadas dizem respeito a tipos de exceção personalizados:
- Nomes de classe de exceção final com
Exception
- Incluir três construtores
- Forneça propriedades adicionais, conforme necessário
Nomes de classe de exceção final com Exception
Quando uma exceção personalizada for necessária, nomeie-a apropriadamente e derive-a da Exception classe. Por exemplo:
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
Inherits Exception
End Class
Incluir três construtores
Use pelo menos os três construtores comuns ao criar suas próprias classes de exceção: o construtor sem parâmetros, um construtor que usa uma mensagem de cadeia de caracteres e um construtor que usa uma mensagem de cadeia de caracteres e uma exceção interna.
- Exception(), que usa valores padrão.
- Exception(String), que aceita uma mensagem de cadeia de caracteres.
- Exception(String, Exception), que aceita uma mensagem de cadeia de caracteres e uma exceção interna.
Para obter um exemplo, consulte Como criar exceções definidas pelo usuário.
Forneça propriedades adicionais, conforme necessário
Forneça propriedades adicionais para uma exceção (além da cadeia de caracteres de mensagem personalizada) somente quando houver um cenário programático em que as informações adicionais sejam úteis. Por exemplo, o FileNotFoundException fornece a FileName propriedade.