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"功能是否会使异常处理更快,因为处理程序会被跳过,并且堆栈展开可能会更早发生?是否有更适合此功能的特定用例,人们可以将其作为一种良好的实践?
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
子句中,工厂函数可以知道发生了什么事。