Detectar excepciones con "catch, when"

94

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 nuevamente algo que puedo hacer dentro del mismo controlador y delegar a diferentes métodos dependiendo del tipo de controlador. ¿Esto hace que el código sea más fácil de entender? Podría decirse que 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 en comparación con el manejo de casos de uso específicos dentro del controlador? ¿Existen casos de uso específicos que se ajusten mejor a esta función y que las personas puedan adoptar como una buena práctica?

MS Srikkanth
fuente
8
Es útil si whennecesita acceder a la excepción en sí
Tim Schmelter
1
Pero eso es algo que también podemos hacer dentro del propio bloque del controlador. ¿Hay algún beneficio aparte de un 'código un poco más organizado'?
MS Srikkanth
3
Pero entonces ya ha manejado la excepción que no desea. ¿Qué pasa si quieres atraparlo en algún otro lugar de esto try..catch...catch..catch..finally?
Tim Schmelter
4
@ user3493289: Siguiendo ese argumento, tampoco necesitamos verificaciones de tipo automáticas en los manejadores de excepciones: solo podemos permitir catch (Exception ex), verificar el tipo y lo throwcontrario. El código un poco más organizado (también conocido como evitar el ruido del código) es exactamente la razón por la que existe esta función. (Esto es cierto para muchas funciones.)
Heinzi
2
@TimSchmelter Gracias. Publícalo como respuesta y lo aceptaré. Entonces, el escenario real sería 'si la condición para el manejo depende de la excepción', entonces use esta función /
MS Srikkanth

Respuestas:

118

Los bloques de captura ya le permiten filtrar por el tipo de excepción:

catch (SomeSpecificExceptionType e) {...}

La whencláusula le permite extender este filtro a expresiones genéricas.

Por lo tanto, usa la whencláusula para los casos en los que el tipo de excepción no es lo suficientemente distinto para determinar si la excepción debe manejarse aquí o no.


Un caso de uso común son los tipos de excepción, que en realidad son un contenedor para múltiples tipos de errores diferentes.

Aquí hay un caso que realmente he usado (en VB, que ya tiene esta función durante 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 ErrorCodepropiedad. 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:

try
{
    SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
    ...
}
catch (COMException e) when (e.ErrorCode == 0x5678)
{
    ...
}

lo que no sería posible sin la whencondición.

Heinzi
fuente
2
El segundo enfoque tampoco permite atraparlo de una manera diferente catch, ¿verdad?
Tim Schmelter
@TimSchmelter. Cierto. Tendría que manejar todas las COMExceptions en el mismo bloque.
Heinzi
Mientras whenque le permite manejar el mismo tipo de excepción varias veces. Deberías mencionar eso también, ya que es una diferencia crucial. Sin whenobtendrá un error del compilador.
Tim Schmelter
1
En lo que a mí respecta, la parte que sigue a "En pocas palabras:" debería ser la primera línea de la respuesta.
CompuChip
1
@ user3493289: sin embargo, ese es a menudo el caso con el código feo. Piensas "No debería estar en este lío en primer lugar, rediseña el código", y también piensas que "podría haber una manera de apoyar este diseño con elegancia, rediseñar el lenguaje". En este caso, hay una especie de umbral para lo feo que desea que sea su conjunto de cláusulas de captura, por lo que algo que hace que ciertas situaciones sean menos feas le permite hacer más dentro de su umbral :-)
Steve Jessop
37

De la wiki de Roslyn (el énfasis es mío):

Los filtros de excepción son preferibles a la captura y el relanzamiento porque dejan la pila ilesa . Si la excepción hace que la pila se vuelque más tarde, 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" utilizar filtros de excepción para los efectos secundarios; por ejemplo, tala. 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 Bel marco de:

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 por caída.

Eli Arbel
fuente
7
Eso me sorprende. ¿No dejará la pila ilesa throw;(a diferencia de throw ex;) también? +1 por el efecto secundario. No estoy seguro de aprobar eso, pero es bueno conocer esa técnica.
Heinzi
13
No está mal, esto no se refiere al seguimiento de la pila , se refiere a la pila en sí. Si observa la pila en un depurador (WinDbg), e incluso si la ha usado throw;, la pila se desenrolla y pierde los valores de los parámetros.
Eli Arbel
1
Esto puede resultar muy útil al depurar volcados.
Eli Arbel
3
@Heinzi Vea mi respuesta en otro hilo donde puede ver que throw;cambia un poco el seguimiento de la pila y lo throw ex;cambia mucho.
Jeppe Stig Nielsen
1
El uso throwperturba ligeramente el rastro de la pila. Los números de línea son diferentes cuando se usan throwen contraposición a when.
Mike Zboray
7

Cuando se lanza una excepción, el primer paso del manejo de excepciones identifica dónde se detectará la excepción antes de desenrollar la pila; si / cuando se identifica la ubicación "captura", todos los bloques "finalmente" se ejecutan (tenga en cuenta que si una excepción escapa de 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 la "captura".

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 ocurra cualquier desenrollado de la pila; por el contrario, un punto de interrupción en una "captura" solo suspenderá la ejecución después de que se finallyhayan ejecutado todos los controladores.

Finalmente, si las líneas 23 y 27 de la foollamada bar, y la llamada en la línea 23 arroja una excepción que se foodetecta dentro y se vuelve a lanzar en la línea 57, el seguimiento de la pila sugerirá que la excepción ocurrió mientras se llamaba bardesde la línea 57 [ubicación de la repetición] , destruyendo cualquier información sobre si la excepción ocurrió en la llamada de la línea 23 o la línea 27. Usar whenpara evitar detectar una excepción en primer lugar evita tal perturbación.

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 whencláusula para establecer una variable que se puede usar dentro de una finallycláusula para determinar si la función se completó normalmente, para manejar casos en los que una función no tiene esperanzas de "resolver" ninguna excepción que se produzca, pero, no obstante, 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 adquirió deberá liberarse, pero la excepción subyacente debería filtrarse hasta el llamador. La forma más limpia de manejar eso semánticamente (aunque no sintácticamente) es tener unfinallybloquear compruebe si se produjo una excepción y, de ser así, libere todos los recursos adquiridos en nombre del objeto que ya no se devolverá. Dado que el código de limpieza no tiene ninguna esperanza de resolver la condición que causó la excepción, realmente no debería catchhacerlo, solo necesita saber qué sucedió. Llamar a una función como:

bool CopySecondArgumentToFirstAndReturnFalse<T>(ref T first, T second)
{
  first = second;
  return false;
}

dentro de una whencláusula hará posible que la función de fábrica sepa que sucedió algo.

Super gato
fuente