使用“ catch,when”捕获异常

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

Again this is something that I can do like:

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

使用“ catch,when”特性是否会使异常处理更快,因为与处理处理程序中的特定用例相比,处理程序本身被跳过,而且堆栈解除可以更早地发生?有没有更适合这个特性的特定用例,人们可以将其作为一个好的实践来采用?

60510 次浏览

Catch blocks already allow you to filter on the 类型 of the exception:

catch (SomeSpecificExceptionType e) {...}

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

因此,对于异常的 < em > type 不够明显的情况,可以使用 when子句来确定是否应该在这里处理异常。


常见的用例是异常类型,它们实际上是针对多种不同类型错误的 包装纸

Here's a case that I've actually used (in VB, which already has this feature for quite some time):

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

Same for SqlException, which also has an ErrorCode property. The alternative would be something like that:

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

可以说是不那么优雅和 稍微破坏了堆栈跟踪

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

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

如果没有 when条件,这是不可能的。

来自罗斯林的 wiki(重点是我的) :

异常筛选器优于捕获和重新引发,因为 如果异常后来导致堆栈 to be dumped, you can see where it originally came from, rather than 只是最后一个被丢弃的地方。

It is also a common and accepted form of “abuse” to use exception 过滤器的副作用; 例如日志记录。他们可以 < strong > 检查异常 在这种情况下,飞机会在没有拦截它的航线的情况下“飞过” Filter 通常是对假返回的 helper 函数的调用,这个函数 执行副作用:

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


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

第一点值得论证。

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行[ rethrow 的位置]调用 bar时发生了异常,从而销毁关于异常是发生在第23行还是第27行调用中的所有信息。使用 when首先避免捕获异常可以避免这种干扰。

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

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

when子句中将使工厂函数可以知道 发生了一些事。