为什么C#编译器允许在catch中使用"throw ex",并且有没有一种情况下"throw ex"是有用的?

6
在 C# 中,年轻的开发者经常使用 "throw ex" 而不是 "throw" 将异常抛到父方法中。
例如:
try
{
    // do stuff that can fail
}
catch (Exception ex)
{
    // do stuff
    throw ex;
}

"throw ex" 是一种不良实践,因为堆栈跟踪会在失败的方法下截断。这样就更难调试代码。因此,代码应该是:

try
{
    // do stuff that can fail
}
catch (Exception ex)
{
    // do stuff
    throw;
}

我的问题是,为什么编译器会授权这个(或者不显示警告消息?) "throw ex" 有用的情况是什么?


12
编译器并不是用来捕捉糟糕的编程习惯的。 - Tyler
1
当你确实想要使用 throw ex 时怎么办? - DavidG
@jgauffin 我想D Stanley的回答已经为我解决了这个问题... - DavidG
1
更准确地说,你的问题应该是“为什么C#编译器不会阻止你在某些位置使用特定的结构(比如throw ex)”,显而易见的答案是,他们为什么要花时间专门修改编译器来防止这种情况呢? - Damien_The_Unbeliever
抓住空的 catch(Exception){} 真正困难的情况不是更重要吗?这些情况可能真的很难调试。但如果我们不能忽略 ThreadAbortException,那会多么烦人呢? - user645280
在思考一天后,又有了一个想法:我认为catch(){throw;}通常也是不好的实践,因为finally{}通常更合适,除非你绝对需要知道异常中的细节......(事实上,我写了很多的finally语句,但是我很少用到catch)老实说,审查大量代码时,我会首先查找catch() - user645280
4个回答

15

“throw ex”有什么用处吗?

当然 - 有时您可能想要截断堆栈跟踪 - 避免公开实现细节等。其他时候,您可能需要抛出一个新的异常,这意味着编译器必须区分仅重新抛出已捕获的异常和抛出新异常。

那么为什么您希望编译器阻止您做某些事情,这些事情1)不是非法的,2)可能很有用?


4

throw new Exception();或者throw ex;都使用相同的语言规则来允许抛出异常对象(无论是新的还是现有的)。当您想要添加一些额外信息到异常中时,这个选项很有用。

请参见:如何:显式抛出异常 - MSDN

您可以使用throw语句显式地抛出异常。您也可以使用throw语句再次抛出捕获的异常。在重新抛出异常时添加信息以提供更多调试信息是良好的编码实践。

由于throw new Exception()throw ex;都需要相同的语言规则,因此编译器并不真正区分这两者。

如果没有对异常对象进行任何修改,仅仅简单地抛出现有的异常,则将使用相同的语言结构。

另外,正如@D Stanley在他的回答中指出的,截断堆栈跟踪可能是期望的行为。

就你关于编译器没有警告的问题而言,编译器的工作并不是警告有问题的做法,这需要代码分析工具。例如,托管代码分析工具会对throw ex;发出警告CA2200:重新引发以保留堆栈详细信息

2
这将是在新异常中“包装”异常,而不仅仅是重新抛出相同的(可能是不可变的)异常对象。 - user645280
@ebyrob,throw new Exceptionthrow ex需要相同的语言结构,我在回答中没有解释清楚。谢谢。 - Habib
为什么对编译器的能力有这么多猜测?如果它可以区分 if( x = 1 )if( x == 1 ) 并发出警告,难道你不认为它可以处理这种情况吗?如果值得追求的话。 - user645280
2
@ebyrob,这不是猜测,if(x=1)if(x == 1)是不同的东西。另一方面,throw可以接收异常对象。现在,该异常对象可以是新的,也可以是已捕获的。 - Habib
@ebyrob,我不知道你这个争论要表达什么。throw (Exception)(object)(x > 1); 是正确的,因为 throw 接受一个返回异常对象的表达式。(尽管在运行时会抛出异常)。但是 if(x = 1) 是错误的,因为这个表达式不返回一个布尔值。这就是它们不同的原因。 - Habib
@Habib 抱歉,显然我最近一直在写 C++。if( b = true ) 是有效的,特别是当 true 被函数调用替换为 if( ret = WillThisSucceed() ) 时。它会在成功时给你一个代码块,并且你不会丢弃返回值。 它也带有一个警告,可能与这种情况实现的难度相同...(并且几乎同样可疑) - user645280

1
虽然编译器确实可以防止一些明显的编程错误,但它们不可能在不触发一些不可避免的误报的情况下注意到最佳实践。
程序员可以选择更改异常处理程序中异常的内容,或者抛出一个全新的异常。在这两种情况下,警告关于从异常处理程序抛出异常的消息都是令人烦恼和无用的。
当您从递归函数中抛出异常时,有一种情况需要更改异常的内部状态。考虑一个递归下降解析器从递归链的几层下报告错误的情况。每个调用级别都可以向异常添加更多有用的信息。但在这种情况下,将每个后续层的异常包装成一个新的异常是不切实际的,因为您最终得到的是表示平面列表的递归数据结构。在这种情况下,一个可行的解决方案是创建一个自定义异常,每个捕获器都可以在重新抛出异常之前添加更多详细信息。由于函数(或更准确地说,一组函数)是递归的,原始异常被抛出的代码位置比导致异常的上下文的完整性不那么重要。

这并不意味着发现这样的情况完全没有用处:代码审查工具(如ReSharper)确实可以帮助程序员监测此类问题。然而,对于最大部分情况来说,编译器应该按照指示执行,因此编译器不是最佳实践监察者的理想选择。


纯属好奇,你用了哪些异常类允许在构造后改变其内部状态?(另一个回答中提到的异常链接是在捕获异常时添加额外信息的标准方法) - user645280
不要试图重复造轮子,你可以使用标准的AggregateException: http://msdn.microsoft.com/en-us/library/system.aggregateexception%28v=vs.110%29.aspx。注意,这也是不可变的。InnerExceptions只能在构造时填充。请注意:捕获1个异常100次(无论是否递归)可能是不好的做法。最好在非常高的层面上捕获并保留足够的状态以创建一个有用的描述。 - user645280
1
@ebyrob,将一个设计上可变的类替换为不可变的类如何是“重复造轮子”?当然,可以围绕AggregateException构建代码以丢弃旧实例并在进行时构建新实例,但这与通过连接不可变字符串模拟可变性是相同的。AggregateException适用于迭代情况,但对于递归情况来说并不那么好。 - Sergey Kalinichenko
1
好的,我猜我错了... Exception 上有一个 .Data 属性: http://msdn.microsoft.com/en-us/library/system.exception.data%28v=vs.110%29.aspx 这看起来比毫无例子地挥手示意“递归难”要有用得多。甚至MSDN文章通过名称重新抛出了 Exception - user645280
编写异常类被称为“子类化”,而不是“设计”。提升用户警告/错误为一流对象,留下异常供库开发人员出错时使用,你说呢? - user645280
显示剩余3条评论

0
我想,当你实际上只想要某个东西沿着堆栈传播时,我认为 catch 是罪魁祸首。不应该完全避免使用 catch 而使用 finally 吗?
bool bSucceeded = false;
try
{
    // do stuff that can fail
    bSucceeded = true;
}
finally
{
    if( !bSucceeded ) 
        // do stuff you need to do only on error.  (rare, for me)

    // cleanup stuff (you're nearly always doing this anyways right?)
}

我写了很多 bSucceded,并且认为我从来没有在 catch 中写过未包装在新异常中的 throw。(至少自从我大约在'99年学习Java以来如此。)

我猜这里有大量可能的处理方式,这就是为什么他们让你随心所欲而不是试图锁定它的原因。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接