Procedimientos recomendados para excepciones
Un control de excepciones adecuado es fundamental para la confiabilidad de las aplicaciones. Puede controlar intencionadamente las excepciones esperadas para evitar que la aplicación se bloquee. Sin embargo, una aplicación bloqueada es más confiable y diagnosticable que una aplicación con un comportamiento indefinido.
En este artículo se describen los procedimientos recomendados para controlar y crear excepciones.
Control de excepciones
Los siguientes procedimientos recomendados están relacionados con cómo se controlan las excepciones:
- Usar bloques try/catch/finally para recuperarse de errores o liberar recursos
- Controlar condiciones comunes para evitar excepciones
- Detectar excepciones de cancelación y asincrónicas
- Diseñar clases para que se puedan evitar excepciones
- Restaurar el estado cuando los métodos no se completan debido a excepciones
- Capturar y volver a iniciar excepciones correctamente
Uso de bloques try/catch/finally para recuperarse de errores o liberar recursos
En el caso del código que podría generar una excepción y cuando la aplicación pueda recuperarse de dicha excepción, use bloques try
/catch
alrededor del código Ordene siempre las excepciones de los bloques catch
de la más derivada a la menos. (todas las excepciones se derivan de la clase Exception. Las excepciones más derivadas no las controla una cláusula catch
que está precedida por una cláusula catch
para una clase de excepción base). Cuando el código no puede recuperarse de una excepción, no capture esa excepción. Habilite los métodos más arriba en la pila de llamadas para recuperarse si es posible.
Limpie los recursos asignados con instrucciones using
o bloques finally
. Dé prioridad a las instrucciones using
para limpiar automáticamente los recursos cuando se produzcan excepciones. Use bloques finally
para limpiar los recursos que no implementan IDisposable. El código de una cláusula finally
casi siempre se ejecuta incluso cuando se producen excepciones.
Controlar condiciones comunes para evitar excepciones
Para las condiciones con probabilidad de producirse, pero que podrían desencadenar una excepción, considere la posibilidad de controlarlas de forma que se evite la excepción. Por ejemplo, si intenta se cerrar una conexión que ya está cerrada, obtendrá un elemento InvalidOperationException
. Se puede evitar mediante una instrucción if
para comprobar el estado de conexión antes de intentar cerrarla.
if (conn.State != ConnectionState.Closed)
{
conn.Close();
}
If conn.State <> ConnectionState.Closed Then
conn.Close()
End IF
Si no comprueba el estado de la conexión antes de cerrar, se puede detectar la excepción 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
El enfoque que se elija depende de la frecuencia con la que espera que se produzca el evento.
Utilice el control de excepciones si el evento no se produce con frecuencia, es decir, si el evento es muy excepcional e indica un error (como un fin de archivo inesperado). Cuando se usa el control de excepciones, se ejecuta menos código en condiciones normales.
Compruebe condiciones de error en el código cuando el evento se produce con frecuencia y se puede considerar como parte de la ejecución normal. Cuando se buscan condiciones de error comunes, se ejecuta menos código porque se evitan excepciones.
Nota:
En la mayoría de los casos se eliminan las excepciones durante las comprobaciones iniciales. Sin embargo, puede haber condiciones de carrera en las que la condición protegida cambie entre la comprobación y la operación y, en ese caso, todavía podría generar una excepción.
Llamar a los métodos Try*
para evitar excepciones
Si el costo de rendimiento de las excepciones es prohibitivo, algunos métodos de biblioteca de .NET proporcionan formas alternativas de control de errores. Por ejemplo, Int32.Parse produce una excepción OverflowException si el valor que se va a analizar es demasiado grande para que Int32 lo represente. Sin embargo, Int32.TryParse no produce esta excepción. En su lugar, devuelve un booleano y tiene un parámetro out
que contiene el entero válido analizado cuando se realiza correctamente. Dictionary<TKey,TValue>.TryGetValue tiene un comportamiento similar para intentar obtener un valor de un diccionario.
Detectar excepciones de cancelación y asincrónicas
Es mejor detectar OperationCanceledException en lugar de TaskCanceledException, que se deriva de OperationCanceledException
, cuando se llama a un método asincrónico. Muchos métodos asincrónicos producen una excepción OperationCanceledException si se solicita la cancelación. Estas excepciones permiten detener la ejecución de forma eficaz y desenredar la pila de llamadas una vez que se observa una solicitud de cancelación.
Los métodos asincrónicos almacenan excepciones que se producen durante la ejecución en la tarea que devuelven. Si se almacena una excepción en la tarea devuelta, se producirá esa excepción cuando se espere la tarea. Las excepciones de uso, como ArgumentException, todavía se producen de forma sincrónica. Para obtener más información, consulte Excepciones asincrónicas.
Diseñar clases para que se puedan evitar excepciones
Una clase puede proporcionar métodos o propiedades que permiten evitar realizar una llamada que desencadenaría una excepción. Por ejemplo, la clase FileStream proporciona métodos que ayudan a determinar si se ha alcanzado el final del archivo. Puede llamar a estos métodos para evitar la excepción que se inicia si se lee más allá del final del archivo. En el ejemplo siguiente se muestra cómo leer hasta el final de un archivo sin desencadenar una excepción:
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
Otro modo de evitar excepciones es devolver null
(o el valor predeterminado) para los casos de errores más comunes en lugar de iniciar una excepción. Un caso de error común se puede considerar como un flujo de control normal. Al devolver null
(o el valor predeterminado) en estos casos, se minimiza el impacto en el rendimiento de una aplicación.
En el caso de los tipos de valor, considere la posibilidad de usar Nullable<T>
o default
como indicador de error de la aplicación. Al utilizar Nullable<Guid>
, default
se convierte en null
en lugar de Guid.Empty
. Algunas veces, agregar Nullable<T>
puede aclarar cuándo un valor está presente o ausente. Otras veces, agregar Nullable<T>
puede crear casos adicionales a fin de comprobar que no son necesarios, y solo sirven para crear posibles orígenes de errores.
Restauración del estado cuando los métodos no se completan debido a excepciones
Los autores de llamadas deben poder asumir que no se producen efectos no deseados cuando se produce una excepción desde un método. Por ejemplo, si tiene código que transfiere dinero mediante la retirada de una cuenta y el depósito en otra, y se inicia una excepción mientras se ejecuta el depósito, no quiere que la retirada siga siendo efectiva.
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
El método anterior no genera directamente ninguna excepción. Pero debe escribir el método para que se revierta la retirada si se produce un error en la operación de depósito.
Para controlar esta situación se puede detectar cualquier excepción iniciada por la transacción del depósito y revertir la retirada.
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
En este ejemplo se muestra el uso de throw
para volver a generar la excepción original, lo que facilita a los autores de llamadas ver la causa real del problema sin tener que examinar la propiedad InnerException. Como alternativa, se puede iniciar una excepción nueva e incluir la original como excepción 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 y volver a iniciar excepciones correctamente
Cuando se inicia una excepción, parte de la información que contiene es el seguimiento de la pila. El seguimiento de la pila es una lista de la jerarquía de llamadas de método que comienza con el método que inicia la excepción y termina con el que la captura. Si se vuelve a iniciar una excepción mediante la especificación de la excepción en la instrucción throw
, por ejemplo, throw e
, el seguimiento de la pila se reinicia en el método actual y se pierde la lista de llamadas de método entre el método original que ha generado la excepción y el método actual. Para mantener la información de seguimiento de la pila original con la excepción, hay dos opciones que dependen de la ubicación desde la que vuelve a iniciar la excepción:
- Si vuelve a iniciar la excepción desde el controlador (bloque
catch
) que ha detectado la instancia de excepción, use la instrucciónthrow
sin especificar la excepción. La regla de análisis de código CA2200 ayuda a buscar ubicaciones en el código donde podría perder accidentalmente la información de seguimiento de la pila. - Si vuelve a generar la excepción desde un lugar distinto del controlador (bloque
catch
), use ExceptionDispatchInfo.Capture(Exception) para capturar la excepción en el controlador y ExceptionDispatchInfo.Throw() cuando quiera volver a generarla. Puede usar la propiedad ExceptionDispatchInfo.SourceException para inspeccionar la excepción capturada.
En el ejemplo siguiente se muestra cómo se puede usar la clase ExceptionDispatchInfo y cuál podría ser el aspecto de la salida.
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();
Si el archivo del código de ejemplo no existe, se genera la salida siguiente:
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
Iniciar excepciones
Los siguientes procedimientos recomendados están relacionados con cómo se inician las excepciones:
- Usar tipos de excepción predefinidos
- Usar métodos de generador de excepciones
- Incluir un mensaje de cadena localizada
- Usar la gramática adecuada
- Colocar instrucciones throw correctamente
- No emitir excepciones en cláusulas finally
- No emitir excepciones desde ubicaciones inesperadas
- Iniciar excepciones de validación de argumentos de forma sincrónica
Usar tipos de excepción predefinidos
Solo se deben introducir nuevas clases de excepción si no se puede aplicar ningún tipo predefinido. Por ejemplo:
- Genere una excepción InvalidOperationException si un conjunto de propiedades o una llamada de método no resultan apropiados en función del estado actual del objeto.
- Genere una excepción ArgumentException o una de las clases predefinidas que derivan de ArgumentException si se pasan parámetros no válidos.
Nota:
Aunque es mejor usar los tipos de excepción predefinidos siempre que sea posible, no debe provocar algunos tipos de excepción reservados, como AccessViolationException, IndexOutOfRangeException, NullReferenceException y StackOverflowException. Para obtener más información, consulte CA2201: No provocar tipos de excepción reservados.
Usar métodos de generador de excepciones
Es habitual que una clase produzca la misma excepción desde distintos lugares de su implementación. Para evitar el exceso de código, cree un método del asistente que cree la excepción y la devuelva. Por ejemplo:
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
Algunos tipos de excepción de .NET clave tienen estos métodos del asistente throw
estáticos que asignan e inician la excepción. Debe llamar a estos métodos en lugar de construir e iniciar el tipo de excepción correspondiente:
- 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
Sugerencia
Las siguientes reglas de análisis de código pueden ayudar a buscar ubicaciones en el código donde puede aprovechar estos asistentes throw
estáticos: CA1510, CA1511, CA1512y CA1513.
Si va a implementar un método asincrónico, llame a CancellationToken.ThrowIfCancellationRequested() en lugar de comprobar si se solicitó la cancelación y, a continuación, construir e iniciar OperationCanceledException. Para obtener más información, consulte CA2250.
Incluir un mensaje de cadena localizada
El mensaje de error que ve el usuario deriva de la propiedad Exception.Message de la excepción que se ha generado, y no del nombre de la clase de excepción. Normalmente, se asigna un valor a la propiedad Exception.Message pasando la cadena de mensaje al argumento message
de un constructor de excepciones.
Para las aplicaciones localizadas, debe proporcionar una cadena de mensaje localizada para todas las excepciones que la aplicación pueda desencadenar. Use archivos de recursos para proporcionar mensajes de error localizados. Para información sobre la localización de aplicaciones y la recuperación de cadenas localizadas, consulte los siguientes artículos:
- Procedimientos para crear excepciones definidas por el usuario con mensajes de excepción localizados
- Recursos en aplicaciones .NET
- System.Resources.ResourceManager
Usar la gramática adecuada
Escriba frases claras e incluya puntuación final. Todas las oraciones de la cadena asignada a la propiedad Exception.Message deben terminar en punto. Por ejemplo, "La tabla del registro se ha desbordado." usa una gramática y puntuación correctas.
Colocar instrucciones throw correctamente
Coloque instrucciones throw donde el seguimiento de la pila resulte útil. El seguimiento de pila comienza en la instrucción en que se produce la excepción y termina en la instrucción catch
que detecta la excepción.
No emitir excepciones en cláusulas finally
No emita excepciones en cláusulas finally
. Para obtener más información, consulte la regla de análisis de código CA2219.
No emitir excepciones desde ubicaciones inesperadas
Algunos métodos, como Equals
, GetHashCode
y ToString
, constructores estáticos y operadores de igualdad, no deben producir excepciones. Para obtener más información, consulte la regla de análisis de código CA1065.
Iniciar excepciones de validación de argumentos de forma sincrónica
En métodos que devuelven tareas, debe validar los argumentos e iniciar las excepciones correspondientes, como ArgumentException y ArgumentNullException, antes de escribir la parte asincrónica del método. Las excepciones iniciadas en la parte asincrónica del método se almacenan en la tarea devuelta y no surgen hasta que, por ejemplo, se espera la tarea. Para obtener más información, consulte Excepciones en métodos que devuelven tareas.
Tipos de excepción personalizados
Los siguientes procedimientos recomendados están relacionados con los tipos de excepción personalizados:
- Terminar los nombres de clases de excepción con
Exception
- Incluir tres constructores
- Proporcionar propiedades adicionales según sea necesario
Terminar los nombres de clases de excepción con Exception
Cuando se necesite una excepción personalizada, debe ponerse el nombre apropiado y derivarla de la clase Exception. Por ejemplo:
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
Inherits Exception
End Class
Incluir tres constructores
Use al menos tres constructores comunes al crear sus propias clases de excepción: el constructor sin parámetros, un constructor que tome un mensaje de cadena y un constructor que tome un mensaje de cadena y una excepción interna.
- Exception(), que utiliza valores predeterminados.
- Exception(String), que acepta un mensaje de cadena.
- Exception(String, Exception), que acepta un mensaje de cadena y una excepción interna.
Por ejemplo, consulte Cómo: Crear excepciones definidas por el usuario.
Proporcionar propiedades adicionales según sea necesario
Únicamente proporcione información adicional para una excepción, además de la cadena del mensaje personalizado, si hay un escenario de programación en el que dicha información sea útil. Por ejemplo, FileNotFoundException proporciona la propiedad FileName.