c# - transaction - like laravel
Capturando excepciones con "catch, when" (3)
Encontré esta nueva característica en C # que permite que se ejecute un controlador de captura cuando se cumple una condición específica.
int i = 0;
try
{
throw new ArgumentNullException(nameof(i));
}
catch (ArgumentNullException e)
when (i == 1)
{
Console.WriteLine("Caught Argument Null Exception");
}
Estoy tratando de entender cuándo esto puede ser útil.
Un escenario podría ser algo como esto:
try
{
DatabaseUpdate()
}
catch (SQLException e)
when (driver == "MySQL")
{
//MySQL specific error handling and wrapping up the exception
}
catch (SQLException e)
when (driver == "Oracle")
{
//Oracle specific error handling and wrapping up of exception
}
..
pero esto es algo que puedo hacer dentro del mismo controlador y delegar a diferentes métodos según el tipo de controlador. ¿Esto hace que el código sea más fácil de entender? Posiblemente no.
Otro escenario en el que puedo pensar es algo como:
try
{
SomeOperation();
}
catch(SomeException e)
when (Condition == true)
{
//some specific error handling that this layer can handle
}
catch (Exception e) //catchall
{
throw;
}
De nuevo, esto es algo que puedo hacer como:
try
{
SomeOperation();
}
catch(SomeException e)
{
if (condition == true)
{
//some specific error handling that this layer can handle
}
else
throw;
}
¿El uso de la función ''atrapar cuando'' hace que el manejo de excepciones sea más rápido porque el controlador se omite como tal y el desenrollado de la pila puede ocurrir mucho antes que en el manejo de los casos de uso específicos dentro del controlador? ¿Hay algún caso de uso específico que se ajuste mejor a esta característica y que las personas puedan adoptar como buena práctica?
Cuando se lanza una excepción, el primer paso del manejo de excepciones identifica dónde quedará atrapada la excepción antes de desenrollar la pila; si / cuando se identifica la ubicación de "captura", se ejecutan todos los bloques "finalmente" (tenga en cuenta que si una excepción escapa a un bloque "finalmente", el procesamiento de la excepción anterior puede abandonarse). Una vez que eso suceda, el código reanudará la ejecución en "catch".
Si hay un punto de interrupción dentro de una función que se evalúa como parte de un "cuándo", ese punto de interrupción suspenderá la ejecución antes de que se produzca el desbobinado de la pila;
por el contrario, un punto de interrupción en una "captura" solo suspenderá la ejecución después de que todos los controladores
finally
hayan ejecutado.
Finalmente, si las líneas 23 y 27 de
foo
llaman a la
bar
, y la llamada en la línea 23 arroja una excepción que se encuentra dentro de
foo
y se vuelve a lanzar en la línea 57, entonces el seguimiento de la pila sugerirá que la excepción ocurrió mientras se llamaba a la
bar
desde la línea 57 [ubicación del relanzamiento], destruyendo cualquier información sobre si la excepción ocurrió en la llamada de la línea 23 o de la línea 27.
Usar
when
evitar una excepción en primer lugar evita tales molestias.
Por cierto, un patrón útil que es molestamente incómodo tanto en C # como en VB.NET es usar una llamada de función dentro de una cláusula
when
para establecer una variable que se pueda usar dentro de una cláusula
finally
para determinar si la función se completó normalmente, para manejar casos donde una función no tiene la esperanza de "resolver" ninguna excepción que ocurra, pero debe tomar medidas basadas en ella.
Por ejemplo, si se lanza una excepción dentro de un método de fábrica que se supone que devuelve un objeto que encapsula recursos, cualquier recurso que se haya adquirido deberá liberarse, pero la excepción subyacente debería filtrarse hasta la persona que llama.
La forma más limpia de manejar eso semánticamente (aunque no sintácticamente) es hacer una verificación de bloqueo final si se produjo una excepción y, de ser así, liberar todos los recursos adquiridos en nombre del objeto que ya no se devolverá.
Dado que el código de limpieza no tiene la esperanza de resolver cualquier condición que haya causado la excepción, realmente no debería
catch
, sino que simplemente necesita saber qué sucedió.
Llamando a una función como:
bool CopySecondArgumentToFirstAndReturnFalse<T>(ref T first, T second)
{
first = second;
return false;
}
dentro de una cláusula
when
hará posible que la función de fábrica sepa que algo sucedió.
Del wiki de Roslyn (énfasis mío):
Los filtros de excepción son preferibles a atrapar y volver a lanzar porque dejan la pila intacta . Si la excepción más tarde hace que la pila sea volcada, puede ver de dónde vino originalmente, en lugar de solo el último lugar donde se volvió a lanzar.
También es una forma común y aceptada de "abuso" usar filtros de excepción para los efectos secundarios; Por ejemplo, el registro. Pueden inspeccionar una excepción "volando" sin interceptar su curso . En esos casos, el filtro a menudo será una llamada a una función auxiliar de retorno falso que ejecuta los efectos secundarios:
private static bool Log(Exception e) { /* log it */ ; return false; } … try { … } catch (Exception e) when (Log(e)) { }
Vale la pena demostrar el primer punto.
static class Program
{
static void Main(string[] args)
{
A(1);
}
private static void A(int i)
{
try
{
B(i + 1);
}
catch (Exception ex)
{
if (ex.Message != "!")
Console.WriteLine(ex);
else throw;
}
}
private static void B(int i)
{
throw new Exception("!");
}
}
Si ejecutamos esto en WinDbg hasta que se alcance la excepción, e imprimimos la pila usando
!clrstack -i -a
veremos solo el marco de
A
:
003eef10 00a7050d [DEFAULT] Void App.Program.A(I4)
PARAMETERS:
+ int i = 1
LOCALS:
+ System.Exception ex @ 0x23e3178
+ (Error 0x80004005 retrieving local variable ''local_1'')
Sin embargo, si cambiamos el programa para usar
when
:
catch (Exception ex) when (ex.Message != "!")
{
Console.WriteLine(ex);
}
Veremos que la pila también contiene el marco de
B
:
001af2b4 01fb05aa [DEFAULT] Void App.Program.B(I4)
PARAMETERS:
+ int i = 2
LOCALS: (none)
001af2c8 01fb04c1 [DEFAULT] Void App.Program.A(I4)
PARAMETERS:
+ int i = 1
LOCALS:
+ System.Exception ex @ 0x2213178
+ (Error 0x80004005 retrieving local variable ''local_1'')
Esa información puede ser muy útil al depurar volcados de memoria.
Los bloques de captura ya le permiten filtrar por tipo de excepción:
catch (SomeSpecificExceptionType e) {...}
La cláusula
when
permite extender este filtro a expresiones genéricas.
Por lo tanto,
utiliza la cláusula
when
para los casos en que el
tipo
de excepción no es lo suficientemente distinto como para determinar si la excepción se debe manejar aquí o no.
Un caso de uso común son los tipos de excepción que en realidad son un contenedor para múltiples y diferentes tipos de errores.
Aquí hay un caso que realmente he usado (en VB, que ya tiene esta característica desde hace bastante tiempo):
try
{
SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
// Handle the *specific* error I was expecting.
}
Lo mismo para
SqlException
, que también tiene una propiedad
ErrorCode
.
La alternativa sería algo así:
try
{
SomeLegacyComOperation();
}
catch (COMException e)
{
if (e.ErrorCode == 0x1234)
{
// Handle error
}
else
{
throw;
}
}
que es posiblemente menos elegante y rompe ligeramente el rastro de la pila .
Además, puede mencionar el mismo tipo de excepción dos veces en el mismo bloque try-catch-block:
try
{
SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
...
}
catch (COMException e) when (e.ErrorCode == 0x5678)
{
...
}
lo cual no sería posible sin la condición
when
.