Pregunta Captura excepciones con "catch, when"


Me encontré con esta nueva característica en C # que permite que un controlador catch se ejecute 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");
}

Intento entender cuándo esto puede ser útil alguna vez.

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 en 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 así 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:

try
{
    SomeOperation();
}
catch(SomeException e)
{
    if (condition == true)
    {
        //some specific error handling that this layer can handle
    }
    else
        throw;
}

¿El uso de la característica 'catch, when' hace que el manejo de excepciones sea más rápido porque el manejador se omite como tal y el desenrollado de la pila puede ocurrir mucho antes cuando se compara con el manejo de casos de uso específicos dentro del manejador? ¿Hay algún caso de uso específico que se ajuste mejor a esta característica que las personas puedan adoptar como una buena práctica?


76
2017-07-21 07:31


origen


Respuestas:


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

catch (SomeSpecificExceptionType e) {...}

los when cláusula le permite extender este filtro a expresiones genéricas.

Así, Usas el when cláusula para los casos donde tipo de la excepción no es lo suficientemente distinto como 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 envoltura para múltiples tipos diferentes de errores

Aquí hay un caso que realmente he usado (en VB, que ya tiene esta característica por bastante tiempo):

try
{
    SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
    // Handle the *specific* error I was expecting. 
}

Igual por SqlException, que también tiene un ErrorCode propiedad. 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 levemente el rastro de la pila.

Además, puedes mencionar el mismo tipo de excepción dos veces en el mismo try-catch-block:

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

que no sería posible sin el when condición.


82
2017-07-21 07:34



De Roslyn wiki (énfasis mío):

Los filtros de excepción son preferibles a la captura y al reinicio porque   ellos deja la pila ileso. Si la excepción más tarde causa la pila   para ser objeto de dumping, puede ver de dónde vino originalmente, en lugar de   solo el último lugar donde fue derrocado.

También es una forma común y aceptada de "abuso" para usar la excepción   filtros para efectos secundarios; p.ej. explotación florestal. Ellos pueden inspeccionar una excepción   "Volando por" sin interceptar su curso. En esos casos, el   filtro a menudo será una llamada a una función de ayuda de devolución falsa que   ejecuta los efectos secundarios:

private static bool Log(Exception e) { /* log it */ ; return false; }

… try { … } catch (Exception e) when (Log(e)) { }

El primer punto vale la pena demostrarlo.

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 golpee la excepción, e imprimimos la pila usando !clrstack -i -a veremos el justo 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 Bmarco de s:

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 archivos.


33
2017-07-21 07:47



Cuando se lanza una excepción, el primer paso del manejo de excepciones identifica dónde se atrapará la excepción antes de desenrollando 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", se puede abandonar el procesamiento de la excepción anterior). Una vez que eso sucede, el código reanudará la ejecución en el "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 desenrollado de la pila; por el contrario, un punto de interrupción en una "captura" solo suspenderá la ejecución después de todo finally los manejadores se han ejecutado.

Finalmente, si las líneas 23 y 27 de foo llamada bar, y la llamada en la línea 23 arroja una excepción que está atrapada dentro foo y volver a tirar en la línea 57, entonces el seguimiento de la pila sugerirá que la excepción ocurrió durante una llamada bar desde la línea 57 [ubicación del re-lanzamiento], destruyendo cualquier información sobre si la excepción ocurrió en la línea 23 o en la línea 27. Utilizando when para evitar la captura de una excepción en primer lugar evita tal perturbación.

Por cierto, un patrón útil que es molesto incómodo tanto en C # como en VB.NET es usar una llamada de función dentro de un when cláusula para establecer una variable que se puede usar dentro de un finally cláusula para determinar si la función se completó normalmente, para manejar casos donde una función no tiene la esperanza de "resolver" cualquier excepción que ocurra, pero que, 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, será necesario liberar todos los recursos que se adquirieron, pero la excepción subyacente debería filtrarse a la persona que llama. La forma más limpia de manejar eso semánticamente (aunque no sintácticamente) es tener un finally bloquee si se produjo una excepción y, de ser así, libere todos los recursos adquiridos en nombre del objeto que ya no se devolverá. Como el código de limpieza no tiene ninguna esperanza de resolver cualquier condición que haya causado la excepción, realmente no debería catch eso, pero simplemente necesita saber qué sucedió. Llamar a una función como:

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

Dentro de un when cláusula hará posible que la función de fábrica sepa que algo sucedió


5
2017-07-21 15:01