Catching exceptions with "catch, when"

本文关键字:quot when catch exceptions Catching with | 更新日期: 2023-09-27 17:58:11

我在C#中发现了这个新特性,它允许catch处理程序在满足特定条件时执行。

int i = 0;
try
{
    throw new ArgumentNullException(nameof(i));
}
catch (ArgumentNullException e)
when (i == 1)
{
    Console.WriteLine("Caught Argument Null Exception");
}

我正试图弄清楚什么时候这可能有用。

一种情况可能是这样的:

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

但这也是我可以在同一个处理程序中完成的事情,并根据驱动程序的类型委托给不同的方法。这是否使代码更容易理解?可以说没有

我能想到的另一个场景是:

try
{
    SomeOperation();
}
catch(SomeException e)
when (Condition == true)
{
    //some specific error handling that this layer can handle
}
catch (Exception e) //catchall
{
    throw;
}

同样,这是我可以做的事情:

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

与处理程序中的特定用例相比,使用"catch,when"功能是否会使异常处理更快,因为处理程序会被跳过,并且堆栈展开可能会更早发生?是否有更适合此功能的特定用例,人们可以将其作为一种良好的实践?

Catching exceptions with "catch, when"

Catch块已经允许您过滤异常的类型

catch (SomeSpecificExceptionType e) {...}

when子句允许您将此筛选器扩展到泛型表达式。

因此,对于异常的类型不足以确定是否应在此处处理异常的情况,可以使用when子句


一个常见的用例是异常类型,它实际上是用于多种不同类型错误的包装器

下面是我实际使用过的一个案例(在VB中,它已经有这个功能很长一段时间了):

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

SqlException也是如此,它也具有ErrorCode属性。另一种选择是:

try
{
    SomeLegacyComOperation();
}
catch (COMException e)
{
    if (e.ErrorCode == 0x1234)
    {
        // Handle error
    }
    else
    {
        throw;
    }
}

这可以说是不那么优雅,并且稍微打破了堆栈痕迹。

此外,您可以在同一个try-catch块中两次提到相同的类型的异常:

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

这在没有CCD_ 5条件的情况下是不可能的。

来自Roslyn的wiki(强调矿):

异常过滤器比捕获和重新思考更可取,因为它们使堆栈不受伤害。如果异常稍后导致堆栈要被丢弃,你可以看到它最初来自哪里,而不是就在最后一个地方,它被重新抛出。

使用例外也是一种常见且被接受的"滥用"形式副作用过滤器;例如日志记录。他们可以检查异常"飞越"而不拦截其航向。在这些情况下filter通常是对错误返回的helper函数的调用,该函数执行副作用:

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

请注意,这指的是堆栈本身,而不是堆栈跟踪,后者可以通过简单地使用throw;而不是throw ex;来保留,但仍会导致堆栈展开。

第一点值得论证。

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

如果我们在WinDbg中运行它,直到遇到异常,并使用!clrstack -i -a打印堆栈,我们将看到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')

但是,如果我们将程序更改为使用when:

catch (Exception ex) when (ex.Message != "!")
{
    Console.WriteLine(ex);
}

我们将看到堆栈还包含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')

在调试崩溃转储时,这些信息可能非常有用。

当抛出异常时,异常处理的第一步确定在展开堆栈之前,异常将在哪里被捕获;如果/当"catch"位置被识别时,所有的"finally"块都会运行(请注意,如果异常从"finally"块中逃脱,则可能会放弃对早期异常的处理)。一旦发生这种情况,代码将在"catch"处恢复执行。

如果函数中有一个断点被评估为"when"的一部分,则该断点将在任何堆栈展开发生之前暂停执行;相比之下,"catch"处的断点只会在所有finally处理程序运行后挂起执行。

最后,如果foo的第23行和第27行调用bar,并且第23行上的调用抛出一个异常,该异常在foo中捕获并在第57行上重新抛出,则堆栈跟踪将表明该异常是在从第57行[重新抛出的位置]调用bar时发生的,从而破坏关于该异常是发生在第23行还是第27行的调用中的任何信息。首先,使用when来避免捕获异常可以避免这种干扰。

顺便说一句,在C#和VB.NET中,一个令人烦恼的尴尬模式是使用when子句中的函数调用来设置一个变量,该变量可以在finally子句中使用,以确定函数是否正常完成,以处理函数没有希望"解决"任何发生的异常,但必须根据异常采取行动的情况。例如,如果在本应返回封装资源的对象的工厂方法中抛出异常,则需要释放所获取的任何资源,但底层异常应该渗透到调用方。从语义上(尽管不是语法上)处理这一问题的最干净的方法是让finally块检查是否发生了异常,如果发生了,则释放代表不再返回的对象获取的所有资源。由于清理代码不希望解决导致异常的任何条件,所以它真的不应该catch它,而只需要知道发生了什么。调用类似的函数

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

when子句中,工厂函数可以知道发生了什么事。