一次捕获多个异常?

不建议仅捕获System.Exception 。相反,仅应捕获 “已知” 异常。

现在,这有时会导致不必要的重复代码,例如:

try
{
    WebId = new Guid(queryString["web"]);
}
catch (FormatException)
{
    WebId = Guid.Empty;
}
catch (OverflowException)
{
    WebId = Guid.Empty;
}

我想知道:有没有一种方法可以捕获两个异常,并且只调用一次WebId = Guid.Empty

给定的示例非常简单,因为它只是一个GUID 。但是,请想象一下代码中多次修改对象的情况,如果其中一种操作以预期的方式失败,则您想 “重置” 该object 。但是,如果有意外的例外,我仍然想将其提高。

答案

捕获System.Exception并打开类型

catch (Exception ex)            
{                
    if (ex is FormatException || ex is OverflowException)
    {
        WebId = Guid.Empty;
        return;
    }

    throw;
}

编辑:我确实同意其他人的说法,从 C#6.0 开始,异常过滤器现在是一种完美的解决方法: catch (Exception ex) when (ex is ... || ex is ... )

除了我仍然讨厌一字线的布局,并且会像下面这样亲自布置代码。我认为这既实用又美观,因为我相信它可以提高理解力。有些人可能不同意:

catch (Exception ex) when (
    ex is ...
    || ex is ...
    || ex is ...
)

原版的:

我知道我在这里参加聚会有点晚了,但是圣洁的烟...

直截了当地追问,这种重复重复了一个较早的答案,但是如果您真的想对几种异常类型执行共同的操作,并使整个事情保持整洁在一种方法的范围之内,为什么不只使用 lambda / closure / inline 函数可以执行以下操作?我的意思是,很有可能您最终会意识到,您只想将该闭包作为一种单独的方法,即可在整个地方使用。但是这样做将非常容易,而无需在结构上实际更改其余代码。对?

private void TestMethod ()
{
    Action<Exception> errorHandler = ( ex ) => {
        // write to a log, whatever...
    };

    try
    {
        // try some stuff
    }
    catch ( FormatException  ex ) { errorHandler ( ex ); }
    catch ( OverflowException ex ) { errorHandler ( ex ); }
    catch ( ArgumentNullException ex ) { errorHandler ( ex ); }
}

我忍不住想知道( 警告:前面有一点讽刺意味 / 讽刺意味)为什么地球上的所有这些工作基本上只是替换以下内容:

try
{
    // try some stuff
}
catch( FormatException ex ){}
catch( OverflowException ex ){}
catch( ArgumentNullException ex ){}

... 这是下一个代码气味的疯狂变化,我的意思是示例,只是假装您在节省一些击键。

// sorta sucks, let's be honest...
try
{
    // try some stuff
}
catch( Exception ex )
{
    if (ex is FormatException ||
        ex is OverflowException ||
        ex is ArgumentNullException)
    {
        // write to a log, whatever...
        return;
    }
    throw;
}

因为它当然不会自动地更具可读性。

当然,我将/* write to a log, whatever... */ return;的三个相同实例/* write to a log, whatever... */ return;在第一个例子中

但这是我的意思。你们都听说过功能 / 方法,对吗?说真的编写一个通用的ErrorHandler函数,然后从每个 catch 块调用它。

如果您问我,第二个示例(带有ifis关键字)在项目维护阶段的可读性大大降低,同时容易出错。

对于那些相对较不熟悉编程的人来说,维护阶段将占项目整个生命周期的 98.7%或更多,而执行维护工作的拙劣工作几乎可以肯定会是您以外的人。而且他们很有可能会花 50%的时间在工作上骂你的名字。

当然,FxCop 会吠叫您,因此您必须在代码中添加一个属性,该属性与正在运行的程序具有精确的 zip 关系,仅是要告诉 FxCop 忽略一个问题,即 99.9%的情况是完全正确标记。而且,对不起,我可能会弄错了,但是,“ignore” 属性最终是否实际上已编译到您的应用程序中?

将整个if测试放在一行上会使其更具可读性吗?我不这么认为。我的意思是,很久以前,确实有另一位程序员激烈地争辩说,将更多代码放在一行上可以使其 “运行得更快”。但是他当然是疯了。试图向他解释(直截了当 - 这很具有挑战性)解释器或编译器如何将长行拆分成离散的每行一条指令的语句 - 与他继续前进时的结果基本相同只是使代码具有可读性,而不是试图使编译器不那么聪明 - 对其没有任何影响。但是我离题了。

当您再添加三种异常类型(从现在开始的一两个月)时,可读性降低了多少? (答案:它会少了很多可读)。

实际上,主要要点之一是格式化我们每天都在看的文本源代码的大部分要点是使它真正,对其他人来说真的很明显,这是代码运行时实际发生的事情。因为编译器将源代码转换成完全不同的东西,所以不必关心您的代码格式样式。因此,全线式也很烂。

只是说...

// super sucks...
catch( Exception ex )
{
    if ( ex is FormatException || ex is OverflowException || ex is ArgumentNullException )
    {
        // write to a log, whatever...
        return;
    }
    throw;
}

正如其他人指出的那样,您可以在 catch 块内使用if语句来确定正在发生的事情。 C#6 支持异常过滤器,因此可以使用以下内容:

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

MyFilter方法可能看起来像这样:

private bool MyFilter(Exception e)
{
  return e is ArgumentNullException || e is FormatException;
}

另外,也可以内联完成所有操作(when 语句的右侧必须是布尔表达式)。

try { … }
catch (Exception e) when (e is ArgumentNullException || e is FormatException)
{
    …
}

这与在catch块中使用if语句不同,使用异常过滤器不会展开堆栈。

您可以下载Visual Studio 2015进行检查。

如果要继续使用 Visual Studio 2013,可以安装以下 nuget 程序包:

安装包 Microsoft.Net.Compilers

在撰写本文时,其中将包括对 C#6 的支持。

引用此程序包将导致使用程序包中包含的 C#和 Visual Basic 编译器的特定版本(而不是任何系统安装的版本)来构建项目。

不幸的是,在 C#中并非如此,因为您需要使用异常过滤器来执行此操作,并且 C#不会公开 MSIL 的功能。 VB.NET 确实具有此功能,例如

Catch ex As Exception When TypeOf ex Is FormatException OrElse TypeOf ex Is OverflowException

您可以做的是使用匿名函数封装错误代码,然后在这些特定的 catch 块中调用它:

Action onError = () => WebId = Guid.Empty;
try
{
    // something
}
catch (FormatException)
{
    onError();
}
catch (OverflowException)
{
    onError();
}

为了完整起见,从.NET 4.0 开始 ,代码可以重写为:

Guid.TryParse(queryString["web"], out WebId);

TryParse绝不会引发异常,如果格式错误,则将Guid.Empty设置为Guid.Empty并返回 false。


C#7 开始,您可以避免在单独的行上引入变量:

Guid.TryParse(queryString["web"], out Guid webId);

您还可以创建用于解析返回元组的方法,该方法在 4.6 版以上的. NET Framework 中尚不可用:

(bool success, Guid result) TryParseGuid(string input) =>
    (Guid.TryParse(input, out Guid result), result);

并像这样使用它们:

WebId = TryParseGuid(queryString["web"]).result;
// or
var tuple = TryParseGuid(queryString["web"]);
WebId = tuple.success ? tuple.result : DefaultWebId;

当在 C#12 中实现析构参数的解构时,此无用答案的下一个无用更新就会到来。

现在,c#6 + 中提供了异常过滤器。你可以做

try
{
       WebId = new Guid(queryString["web"]);
}
catch (Exception ex) when(ex is FormatException || ex is OverflowException)
{
     WebId = Guid.Empty;
}

在 C#7.0 + 中,您也可以将其与模式匹配结合使用

try
{
   await Task.WaitAll(tasks);
}
catch (Exception ex) when( ex is AggregateException ae &&
                           ae.InnerExceptions.Count > tasks.Count/2)
{
   //More than half of the tasks failed maybe..? 
}

如果您可以将应用程序升级到 C#6,那么您很幸运。新的 C#版本已实现了异常过滤器。所以你可以这样写:

catch (Exception ex) when (ex is FormatException || ex is OverflowException) {
    WebId = Guid.Empty;
}

有人认为此代码与

catch (Exception ex) {                
    if (ex is FormatException || ex is OverflowException) {
        WebId = Guid.Empty;
    }
    throw;
}

但事实并非如此。实际上,这是 C#6 中唯一无法在先前版本中模仿的新功能。首先,重新抛出意味着比跳过捕获更多的开销。其次,它在语义上并不等效。调试代码时,新功能可以使堆栈保持原样。如果没有此功能,则故障转储将变得不太有用甚至没有用。

在 CodePlex 上查看有关此内容讨论 。并举例说明差异

如果不想在catch范围内使用if语句,则C# 6.0 ,可以使用 CLR 在预览版本中已支持但仅在VB.NET / MSIL存在的Exception Filters语法

try
{
    WebId = new Guid(queryString["web"]);
}
catch (Exception exception) when (exception is FormatException || ex is OverflowException)
{
    WebId = Guid.Empty;
}

仅当它是InvalidDataExceptionArgumentNullException时,此代码才捕获Exception

实际上,基本上可以在when子句中放入任何条件:

static int a = 8;

...

catch (Exception exception) when (exception is InvalidDataException && a == 8)
{
    Console.WriteLine("Catch");
}

请注意,与catch范围内的if语句相反, Exception Filters无法引发Exceptions ,并且当它们发出Exceptions时或当条件不为true ,将改为评估下一个catch条件:

static int a = 7;

static int b = 0;

...

try
{
    throw new InvalidDataException();
}
catch (Exception exception) when (exception is InvalidDataException && a / b == 2)
{
    Console.WriteLine("Catch");
}
catch (Exception exception) when (exception is InvalidDataException || exception is ArgumentException)
{
    Console.WriteLine("General catch");
}

输出:一般捕获。

当有一个以上的true Exception Filter - 第一个将被接受:

static int a = 8;

static int b = 4;

...

try
{
    throw new InvalidDataException();
}
catch (Exception exception) when (exception is InvalidDataException && a / b == 2)
{
    Console.WriteLine("Catch");
}
catch (Exception exception) when (exception is InvalidDataException || exception is ArgumentException)
{
    Console.WriteLine("General catch");
}

输出:渔获量。

正如您在MSIL看到的那样,代码不会转换为if语句,而是转换为Filters ,并且不能从标记为Filter 1Filter 2的区域内引发Exceptions ,但引发Exception的过滤器将失败,最后一个Exceptionendfilter命令之前将比较值压入堆栈将确定过滤器的成功 / 失败( Catch 1 XOR Catch 2将相应执行):

异常过滤器MSIL

此外,特别是Guid具有Guid.TryParse方法。

使用 C#7 可以改善Michael Stum 的答案,同时保持 switch 语句的可读性:

catch (Exception ex)
{
    switch (ex)
    {
        case FormatException _:
        case OverflowException _:
            WebId = Guid.Empty;
            break;
        default:
            throw;
    }
}

可接受的答案似乎是可以接受的,除了 CodeAnalysis / FxCop会抱怨它正在捕获通用异常类型这一事实。

另外,似乎 “is” 运算符可能会稍微降低性能。

CA1800:不必进行不必要的强制转换 ,而是 “考虑测试'as'运算符的结果”,但是,如果这样做的话,与单独捕获每个异常的情况相比,您将编写更多的代码。

无论如何,这是我会做的:

bool exThrown = false;

try
{
    // Something
}
catch (FormatException) {
    exThrown = true;
}
catch (OverflowException) {
    exThrown = true;
}

if (exThrown)
{
    // Something else
}