异常过滤器

异常过滤器使开发人员能够将一个条件(以 boolean 表达式的形式)添加到 catch块,允许 catch 仅在条件计算为 true 时执行。

异常过滤器允许在原始异常中传播调试信息,其中在 catch 块内使用 if 语句并重新抛出异常会停止在原始异常中传播调试信息。使用异常过滤器时,异常将继续在调用堆栈中向上传播,除非满足条件。因此,异常过滤器使调试体验变得更加容易。调试器不会停止在 throw 语句上,而是停止抛出异常的语句,并保留当前状态和所有局部变量。崩溃转储以类似的方式受到影响。

CLR 从一开始就支持异常过滤器,并且通过暴露 CLR 的异常处理模型的一部分,它们可以从 VB.NET 和 F#访问十多年。只有在 C#6.0 发布之后,C#开发人员才能使用该功能。

使用异常过滤器

通过将 when 子句附加到 catch 表达式来使用异常过滤器。可以使用在 when 子句中返回 bool 的任何表达式( 等待除外 )。声明的异常变量 ex 可以从 when 子句中访问:

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

可以组合具有 when 子句的多个 catch 块。返回 true 的第一个 when 子句将导致异常被捕获。它的 catch 块将被输入,而其他 catch 块将被忽略(它们的 when 条款将不被评估)。例如:

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
{ ... }

条款危险时

警告

使用异常过滤器可能有风险:当从 when 子句中抛出 Exception 时,when 子句中的 Exception 将被忽略并被视为 false。这种方法允许开发人员在不处理无效情况的情况下编写 when 子句。

以下示例说明了这种情况:

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);
}

查看演示

请注意,当失败代码在同一函数内时,异常过滤器可避免与使用 throw 相关的令人困惑的行号问题。例如,在这种情况下,行号报告为 6 而不是 3:

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

异常行号报告为 6,因为错误是在第 6 行的 throw 语句中捕获并重新抛出的。

异常过滤器不会发生同样的情况:

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

在此示例中,a 为 0,然后忽略 catch 子句,但报告 3 为行号。这是因为它们不会展开堆栈。更具体地说,异常没有在第 5 行捕获,因为 a 实际上确实等于 0,因此没有机会在第 6 行重新抛出异常,因为第 6 行不执行。

记录为副作用

条件中的方法调用可能会导致副作用,因此可以使用异常过滤器在异常上运行代码而不捕获它们。利用这个的一个常见例子是 Log 方法总是返回 false。这允许在调试时跟踪日志信息,而无需重新抛出异常。

**请注意,**虽然这似乎是一种舒适的日志记录方式,但它可能存在风险,尤其是在使用第三方日志记录程序集时。这些可能会在记录可能无法轻易检测到的非显而易见的情况时抛出异常(请参阅上面的 Risky when(...) 子句 )。

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;
}

查看演示

以前版本的 C#中的常见方法是记录并重新抛出异常。

Version < 6

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);
}

查看演示

finally 区块

finally 块执行每次是否引发异常或没有时间。在 when 中使用表达式的一个微妙之处是异常过滤器进入内部 finally之前在堆栈中进一步执行。当代码尝试修改全局状态(如当前线程的用户或文化)并将其设置回 finally 块时,这可能会导致意外的结果和行为。

示例:finally block

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");
    }
}

输出:

开始
评估:真正的
内在终于最终
抓住
外面

查看演示

在上面的例子中,如果方法 SomeOperation 不希望泄漏全局状态改变为调用者的 when 子句,它还应该包含 catch 块来修改状态。例如:

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

通常看到 IDisposable 辅助类利用使用块的语义来实现相同的目标,因为在 using 块内调用的异常开始冒泡堆栈之前,将始终调用 IDisposable.Dispose