C# Language Filtros de excepción


Ejemplo

Los filtros de excepción les dan a los desarrolladores la capacidad de agregar una condición (en forma de expresión boolean ) a un bloque catch , permitiendo que la catch ejecute solo si la condición se evalúa como true .

Los filtros de excepción permiten la propagación de información de depuración en la excepción original, mientras que al usar una instrucción if dentro de un bloque catch y volver a lanzar la excepción, se detiene la propagación de información de depuración en la excepción original. Con los filtros de excepción, la excepción continúa propagándose hacia arriba en la pila de llamadas a menos que se cumpla la condición. Como resultado, los filtros de excepción hacen que la experiencia de depuración sea mucho más fácil. En lugar de detenerse en la declaración de throw , el depurador se detendrá en la instrucción de lanzar la excepción, con el estado actual y todas las variables locales conservadas. Los vertederos se ven afectados de manera similar.

Los filtros de excepción han sido admitidos por el CLR desde el principio y han sido accesibles desde VB.NET y F # durante más de una década al exponer una parte del modelo de manejo de excepciones del CLR. Solo después del lanzamiento de C # 6.0, la funcionalidad también estuvo disponible para los desarrolladores de C #.


Usando filtros de excepción

Los filtros de excepción se utilizan agregando una cláusula when a la expresión catch . Es posible usar cualquier expresión que devuelva un bool en una cláusula when (excepto en espera ). La variable de excepción declarada ex es accesible desde dentro de la cláusula when :

var SqlErrorToIgnore = 123;
try
{
    DoSQLOperations();
}
catch (SqlException ex) when (ex.Number != SqlErrorToIgnore)
{
    throw new Exception("An error occurred accessing the database", ex);
}

Se pueden combinar múltiples bloques catch con las cláusulas when . La primera when cláusula devuelva true provocará que se detecte la excepción. Se ingresará su bloque catch , mientras que las otras cláusulas catch se ignorarán (no se evaluarán sus cláusulas when ). Por ejemplo:

try
{ ... }
catch (Exception ex) when (someCondition) //If someCondition evaluates to true,
                                          //the rest of the catches are ignored.
{ ... }
catch (NotImplementedException ex) when (someMethod()) //someMethod() will only run if
                                                       //someCondition evaluates to false
{ ... }
catch(Exception ex) // If both when clauses evaluate to false
{ ... }

Arriesgado cuando la cláusula

Precaución

Puede ser arriesgado usar filtros de excepción: cuando se lanza una Exception desde dentro de la cláusula when , la Exception de la cláusula when se ignora y se trata como false . Este enfoque permite a los desarrolladores escribir when cláusula sin tener en cuenta los casos no válidos.

El siguiente ejemplo ilustra tal escenario:

public static void Main()
{
    int a = 7;
    int b = 0;
    try
    {
        DoSomethingThatMightFail();
    }
    catch (Exception ex) when (a / b == 0)
    {
        // This block is never reached because a / b throws an ignored
        // DivideByZeroException which is treated as false.
    }
    catch (Exception ex)
    {
        // This block is reached since the DivideByZeroException in the 
        // previous when clause is ignored.
    }
}

public static void DoSomethingThatMightFail()
{
    // This will always throw an ArgumentNullException.
    Type.GetType(null);
}

Ver demostración

Tenga en cuenta que los filtros de excepción evitan los confusos problemas de número de línea asociados con el uso de throw cuando el código que falla está dentro de la misma función. Por ejemplo, en este caso, el número de línea se reporta como 6 en lugar de 3:

1. int a = 0, b = 0;
2. try {
3.     int c = a / b;
4. }
5. catch (DivideByZeroException) {
6.     throw;
7. }

El número de línea de excepción se informa como 6 porque el error se detectó y se volvió a lanzar con la declaración de throw en la línea 6.

Lo mismo no ocurre con los filtros de excepción:

1. int a = 0, b = 0;
2. try {
3.     int c = a / b;
4. }
5. catch (DivideByZeroException) when (a != 0) {
6.     throw;
7. }

En este ejemplo a es 0, la cláusula catch se ignora, pero 3 se reporta como número de línea. Esto se debe a que no desenrollan la pila . Más específicamente, la excepción no se detecta en la línea 5, porque a de hecho es igual a 0 y por lo tanto no hay oportunidad para la excepción que ser re-lanzado en la línea 6, porque la línea 6 no se ejecuta.


La tala como efecto secundario

Las llamadas de método en la condición pueden causar efectos secundarios, por lo que los filtros de excepción se pueden usar para ejecutar código en excepciones sin detectarlos. Un ejemplo común que aprovecha esto es un método de Log que siempre devuelve false . Esto permite rastrear la información de registro mientras se realiza la depuración sin la necesidad de volver a lanzar la excepción.

Tenga en cuenta que si bien esto parece ser una forma cómoda de registro, puede ser riesgoso, especialmente si se utilizan ensamblajes de registro de terceros. Estos pueden generar excepciones al iniciar sesión en situaciones no obvias que pueden no detectarse fácilmente (consulte Risky when(...) cláusula anterior).

try
{
    DoSomethingThatMightFail(s);
}
catch (Exception ex) when (Log(ex, "An error occurred"))
{
    // This catch block will never be reached
}

// ...

static bool Log(Exception ex, string message, params object[] args)
{
    Debug.Print(message, args);
    return false;
}

Ver demostración

El enfoque común en versiones anteriores de C # era registrar y volver a lanzar la excepción.

6.0
try
{
    DoSomethingThatMightFail(s);
}
catch (Exception ex)
{
     Log(ex, "An error occurred");
     throw;
}

// ...

static void Log(Exception ex, string message, params object[] args)
{
    Debug.Print(message, args);
}

Ver demostración


El bloque finally

El bloque finally se ejecuta cada vez si se lanza la excepción o no. Una sutileza con expresiones en when se ejecutan los filtros de excepción más arriba en la pila antes de ingresar a los bloques internos por finally . Esto puede provocar que los resultados y comportamientos inesperados cuando el código intenta modificar el estado global (como usuario o la cultura del hilo actual) y poner de nuevo en un finally de bloquear.

Ejemplo: finally bloque

private static bool Flag = false;

static void Main(string[] args)
{
    Console.WriteLine("Start");
    try
    {
        SomeOperation();
    }
    catch (Exception) when (EvaluatesTo())
    {
        Console.WriteLine("Catch");
    }
    finally
    {
        Console.WriteLine("Outer Finally");
    }
}

private static bool EvaluatesTo()
{
    Console.WriteLine($"EvaluatesTo: {Flag}");
    return true;
}

private static void SomeOperation()
{
    try
    {
        Flag = true;
        throw new Exception("Boom");
    }
    finally
    {
        Flag = false;
        Console.WriteLine("Inner Finally");
    }
}

Salida producida:

comienzo
EvaluatesTo: True
Interior finalmente
Captura
Exterior finalmente

Ver demostración

En el ejemplo anterior, si el método SomeOperation no desea a "fugas" de los cambios de estado globales a quien haya llamado when cláusulas, sino que también deben contener un catch bloque para modificar el estado. Por ejemplo:

private static void SomeOperation()
{
    try
    {
        Flag = true;
        throw new Exception("Boom");
    }
    catch
    {
       Flag = false;
       throw;
    }
    finally
    {
        Flag = false;
        Console.WriteLine("Inner Finally");
    }
}

También es común ver clases de ayuda IDisposable aprovechan la semántica del uso de bloques para lograr el mismo objetivo, ya que IDisposable.Dispose siempre se llamará antes de que una excepción llamada dentro de un bloque using comience a burbujear la pila.