使用“在何时捕获”捕获异常


94

我遇到了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;
}

与处理程序中的特定用例相比,使用“捕获时”功能是否会因为处理程序被跳过而使异常处理更快,并且堆栈展开可能比处理程序中的特定用例早得多?是否有任何更适合此功能的特定用例,然后人们可以采纳这些用例作为良好实践?


8
如果when需要访问异常本身,这很有用
Tim Schmelter

1
但这是我们可以在处理程序块本身中执行的操作。除了“稍微更有条理的代码”之外,还有什么好处吗?
MS Srikkanth '16

3
但是您已经处理了不需要的异常。如果您想在其他地方抓住它try..catch...catch..catch..finally怎么办?
蒂姆·施密特

4
@ user3493289:在该参数之后,我们也不需要在异常处理程序中进行自动类型检查:我们只能允许catch (Exception ex)检查类型,throw否则进行检查。稍微更有条理的代码(也就是避免代码杂音)正是此功能存在的原因。(实际上,很多功能都是这样。)
Heinzi

2
@TimSchmelter谢谢。将其发布为答案,我会接受。所以,实际情况将被“如果处理的条件取决于例外的”,然后使用此功能/
MS Srikkanth

Answers:


118

捕获块已经可以过滤异常类型

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-block中两次提及相同类型的异常:

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

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


2
第二种方法也不允许将其陷入另一种情况catch,是吗?
蒂姆·施密特

@TimSchmelter。真正。您必须在同一块中处理所有 COMException。
Heinzi '16

虽然when允许您多次处理相同的异常类型。您还应该提到这一点,因为这是至关重要的区别。没有它们,when您将得到一个编译器错误。
蒂姆·施密特

1
就我而言,“简而言之:”之后的部分应该是答案的第一行。
CompuChip

1
@ user3493289:丑陋的代码通常是这种情况。您认为“一开始我不应该陷入困境,重新设计代码”,并且您还认为“可能存在一种可以优雅地支持此设计,重新设计语言的方法”。在这种情况下,对于您想要的catch子句集有多丑有一个阈值,因此使某些情况变得不那么丑陋的东西使您可以在阈值之内完成更多工作:-)
Steve Jessop

37

从罗斯林的Wiki(重点是我的):

异常过滤器比捕获和重新抛出更可取,因为它们使堆栈不受损害。如果异常之后导致堆栈被转储,则可以看到堆栈的原始来源,而不仅仅是堆栈被丢弃的最后一个位置。

使用异常过滤器来产生副作用也是一种常见的“滥用”形式。例如记录。他们可以检查“飞过”的异常而不会拦截其过程。在这些情况下,过滤器通常是对执行以下副作用的错误返回帮助函数的调用:

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')

在调试故障转储时,该信息可能非常有用。


7
这让我感到惊讶。throw;(相对于throw ex;)是否也不会损坏堆栈?副作用为+1。我不确定我是否赞成,但是很高兴知道这种技术。
Heinzi

13
没错-这并不涉及堆栈跟踪 -它涉及堆栈本身。如果您在调试器(WinDbg)中查看堆栈,即使您已使用throw;,堆栈也会展开并丢失参数值。
Eli Arbel

1
在调试转储时,这可能非常有用。
Eli Arbel

3
@Heinzi 在另一个线程中查看我的答案,您可以看到该throw;变化稍微改变了堆栈跟踪并throw ex;做了很多更改。
Jeppe Stig Nielsen

1
使用throw确实会稍微干扰堆栈跟踪。行号使用时是不同的throw,而不是when
Mike Zboray '16

7

当引发异常时,异常处理的第一遍将确定展开堆栈之前捕获异常的位置。如果/当识别到“捕获”位置时,将运行所有“最终”块(请注意,如果异常逃脱了“最终”块,则可能会放弃对较早异常的处理)。一旦发生这种情况,代码将在“ catch”处恢复执行。

如果函数中有一个断点被视为“时间”的一部分,则该断点将在执行任何堆栈展开之前暂停执行;相比之下,“捕获”处的断点毕竟只会暂停执行finally处理程序运行。

最后,如果foocall的第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;
}

一个内when子句将有可能使工厂函数知道发生了什么事情。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.