用"捕获,何时"捕获异常

MS *_*nth 85 c# exception-handling

我在C#中遇到了这个新功能,它允许在满足特定条件时执行catch处理程序.

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

我想知道什么时候这可能有用.

一种情况可能是这样的:

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
}
..
Run Code Online (Sandbox Code Playgroud)

但这又是我可以在同一个处理程序中执行的操作,并根据驱动程序的类型委托给不同的方法.这是否使代码更容易理解?可以说没有.

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

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

这也是我能做的事情:

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

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

Hei*_*nzi 106

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

catch (SomeSpecificExceptionType e) {...}
Run Code Online (Sandbox Code Playgroud)

when子句允许您将此过滤器扩展为通用表达式.

因此,对于异常类型不够明确以确定是否应在此处理异常的情况,请使用该when子句.


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

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

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

同样的SqlException,也有一个ErrorCode属性.替代方案将是这样的:

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

可以说它不那么优雅,并略微打破了堆栈的痕迹.

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

try
{
    SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
    ...
}
catch (COMException e) when (e.ErrorCode == 0x5678)
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

没有when条件就不可能.

  • 第二种方法也不允许在不同的"捕获"中捕获它,是吗? (2认同)
  • @user3493289:丑陋的代码通常就是这种情况。你认为“我一开始就不应该陷入这种混乱,重新设计代码”,并且你还认为“可能有一种方法可以优雅地支持这种设计,重新设计语言”。在这种情况下,您希望您的 catch 子句集有多难看有一个阈值,因此可以让某些情况变得不那么难看,让您在阈值内完成更多工作:-) (2认同)

Eli*_*bel 37

来自Roslyn的wiki(强调我的):

异常过滤器比捕获和重新抛出更可取,因为它们可以保持堆栈不受破坏.如果稍后的异常导致堆栈被转储,您可以看到它最初来自哪里,而不仅仅是它重新抛出的最后一个位置.

使用异常过滤器进行副作用也是一种常见且被接受的"滥用"形式; 例如伐木.他们可以在不拦截其路线的情况下检查"飞过"的异常.在这些情况下,过滤器通常会调用一个错误返回的辅助函数来执行副作用:

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

… try { … } catch (Exception e) when (Log(e)) { }
Run Code Online (Sandbox Code Playgroud)

第一点值得证明.

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("!");
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我们在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')
Run Code Online (Sandbox Code Playgroud)

但是,如果我们更改要使用的程序when:

catch (Exception ex) when (ex.Message != "!")
{
    Console.WriteLine(ex);
}
Run Code Online (Sandbox Code Playgroud)

我们将看到堆栈还包含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')
Run Code Online (Sandbox Code Playgroud)

调试崩溃转储时,该信息非常有用.

  • 这没有错 - 这不是指堆栈*跟踪* - 它指的是堆栈本身.如果你在调试器(WinDbg)中查看堆栈,即使你使用了`throw;`,堆栈也会展开并丢失参数值. (13认同)
  • 这让我感到惊讶.不会`throw;`(而不是`throw ex;`)不会让堆栈安然无恙吗?+1为副作用的东西.我不确定我是否同意这一点,但了解这项技术是件好事. (7认同)
  • @Heinzi看到[我在另一个帖子中的回答](http://stackoverflow.com/a/11229760/1336654)你可以看到`throw;`稍微改变堆栈跟踪并且`throw ex;`改变它很多. (3认同)

sup*_*cat 6

抛出异常时,异常处理的第一次传递标识展开堆栈之前将捕获异常的位置; 如果/当识别出"catch"位置时,运行所有"finally"块(注意,如果异常逃脱"finally"块,则可以放弃对先前异常的处理).一旦发生这种情况,代码将在"catch"处恢复执行.

如果在作为"when"的一部分计算的函数中存在断点,则该断点将在任何堆栈展开之前暂停执行; 相比之下,"catch"处的断点只会在所有finally处理程序运行后暂停执行.

最后,如果foo调用的第23行和第27行bar以及第23行的调用抛出了一个异常,它被捕获foo并在第57 bar行重新抛出,那么堆栈跟踪将表明在从第57行调用时发生了异常[重新抛出的位置] ,销毁有关在23行或27行调用中是否发生异常的任何信息.使用when以避免在第一时间捕获异常避免这种干扰.

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

bool CopySecondArgumentToFirstAndReturnFalse<T>(ref T first, T second)
{
  first = second;
  return false;
}
Run Code Online (Sandbox Code Playgroud)

在一个when条款中,工厂功能可以知道发生了什么事.