有没有真正的现实原因使用 throw ex?

18

在C#中,throw ex几乎总是错误的,因为它会重置堆栈跟踪。

我只是想知道,这有没有实际的用途?我能想到的唯一原因是隐藏您封闭库的内部,但那真的很弱。除此之外,我在现实世界中从未遇到过。

编辑:我的意思是throw ex,即抛出与捕获的完全相同的异常,但具有空堆栈跟踪,就像完全错误地执行一样。我知道throw ex必须作为语言结构存在才能允许抛出不同的异常(throw new DifferentException("ex as innerException", ex)),只是想知道是否存在throw ex不错误的情况。


你是真的想要使用 throw ex; 还是只是想要使用 throw; - Andrew Barber
@Andrew 编辑了这个问题。我的意思是 throw ex,只是想知道是否有任何情况下它不算错误的做法。 - Michael Stum
我猜这就是你的意思,但是我认为澄清一下可能会有所帮助,因为似乎有些混淆(例如,假设你错误地意味着throw;)。 - Andrew Barber
1
我知道这很旧了,但是这周我也在考虑同样的事情。看起来这至少是编译器警告的理想候选! - Mr Moose
@MrMoose Damien_The_Unbeliever的回答解释了为什么它不是。 - samus
6个回答

13

我从未见过有人有意使用它,但这当然并不代表什么。

让我们看看其他选择:

catch(Exception ex)
{
    // do stuff (logging)
    throw;                                // a) continue ex
    throw new SomeException("blah", ex);  // b) wrap
    throw new SomeException("blah");      // c) replace
    throw ex;                             // d) reset stack-trace
}

选项 c) 和 d) 都会丢失堆栈跟踪,唯一我能看到 d) 的“优势”就是 throw ex; 会保留 ex 的确切类型和可能的额外属性(例如 SQL 错误)。

那么有没有情况需要保留异常的所有信息但不包括堆栈跟踪呢?通常不需要,但有些猜测:

  • 在模糊库边界处。但是这种情况下,选项 c) 似乎是更好的选择。
  • 在调用某些生成代码的调用点上,比如 RegEx 类中。

2
我只能说,如果你要杀掉堆栈跟踪以保护你的库知识产权,那么你最好给我一个相当好的异常消息。 :) - Paul
1
@Paul:是的,就像楼主已经说过的那样,这样做会相当无聊。 - H H
有时在使用注入策略来确定如何处理异常的异常块中,您会看到 throw excatch( Exception ex ) { ExceptionStrategy( ex ); } ... void ExceptionStrategy( Exception ex ) { /* 确定是否可以处理/恢复,否则抛出 ex*/ } ... 但这种情况很少见。 - LBushkin

5

如果你仔细思考,throw ex实际上经常被使用——只是用于未命名的本地变量。重新设计语言以便:

throw new ArgumentException();

是有效的,但是

var ex = new ArgumentException();
throw ex;

如果要跟踪更复杂的变量分配情况,可以使用throw ex语句来检测变量是否无效。也就是说,throw ex语句是throw语句的“正常”形式,但在重新抛出已捕获的异常时通常是错误的。


9
我认为OP的意思是,在重新抛出异常时是否应该使用throw ex; - jgauffin

4

没有合理的理由重新抛出异常对象,即'throw ex'。

虽然这是一种可能的语言\编译器特性,但实际上不会为负面影响增加任何价值。这与能够编写代码来访问空引用成员的情况类似 - 当然可以实现,但没有价值,例如:

    //Possible but not useful.
    object ref1 = null;

    ref1.ToString();

能够重新抛出异常对象是不幸的,往往被新手和有经验的编码人员误解,直到在生产环境中记录截断的堆栈跟踪日志时才意识到这一点。
有一种脆弱的感觉,认为可以故意隐藏堆栈跟踪信息,但抛出新异常才是正确的方法。
我甚至认为能够重新抛出异常对象(即'throw ex')是设计缺陷——做错事情太容易了。然而,我怀疑这是编译器性能上的一个设计权衡,因为验证“throw ex”是否是“重新抛出”会产生开销。我相信有许多这样的权衡。

不确定我是否会将其与调用“null”引用上的成员进行比较;这会给您带来运行时错误(故意避免使用“异常”一词,以避免混淆),而使用throw ex;重新抛出异常将可以正常工作,无论它有多可疑。 - Andrew Barber
@Andrew 这个例子说明了编程语言/编译器可能允许你表达一些没有或者很有限价值的东西。有些编程语言/编译器功能可以让你做一些明显错误的事情,但这些错误可能被忽略或出于其他原因而被允许,例如太昂贵的验证成本等。 - Tim Lloyd
@chibacity - 我知道;但我认为空引用问题实际上比没有值更糟糕;它实际上可能会导致程序崩溃。 - Andrew Barber
@Andrew 是的,我同意,但我想在这里描绘一个更大的画面 :) 所以你认为在生产中拥有重新启动的堆栈跟踪是一件好事!“应用程序已崩溃,但很棒,我们有了堆栈跟踪 - 噢不,一些混蛋已经“throw ex”了,我们没有堆栈跟踪 - grrr”。 - Tim Lloyd
嗯...有趣;我不记得说过这是一件好事。但是通过throw ex;重新抛出异常本身不会导致程序崩溃;尝试在空引用上使用成员可能会导致程序崩溃。更好的比较可能是与其他表面上“无害”的行为相比,这些行为来自一些通常不建议使用的模式。 - Andrew Barber
但是重新抛出异常对象有一个有效的理由:解包并重新抛出内部异常。在处理任务时,这种需求经常会出现,因为执行期间抛出的异常被包装在AggregateException中。 - Søren Boisen

2
throw ex只是由一些不知道或者忘记throw;的人犯下的错误或打字错误。
它可能只有在一个情况下才会被使用,即你不想将适当的堆栈跟踪发送给调用者,但你仍然希望保留之前抛出的异常类型。实际上,我几乎无法想象有人会需要它,并且不使用throw new SomeCustomException(...)代替它。
FxCop规则 CA2200 RethrowToPreserveStackDetails 也是这样做的,通过使用throw;来正确地重新抛出异常
try
{
    // ...
}
catch (SomeException ex)
{
    Logger.AddException(ex);
    throw;
}

@fejesjoco:问题是:“这个有实际应用吗?”我的答案在最后一段(注释之前)。 - Arseni Mourzenko
我仍然不理解为什么对于一个实际上有效回答问题的回答会被踩。如果只是因为我的回答开头没有回答这个问题,那么在投赞成票/反对票之前,你应该先读完整个回答。 - Arseni Mourzenko
我同意你的观点,MainMa。我曾经遇到过这样的情况,需要重新抛出异常但同时也要记录日志。使用 throw 会保留被抛出异常的行号(即堆栈跟踪),而使用 throw ex 则会改变行号。 - taher chhabrawala
解释如何正确使用throw并不是问题的重点。 - Tim Lloyd
@chibacity:我想你没有读完整个答案。在所有情况下,我将我的答案的最后一句话移到了开头,所以现在应该更清楚了。 - Arseni Mourzenko
@MainMan 当然我读完了你的整个回答 - 只是在你编辑之前它不是很好。现在你主要在总结其他人的答案。 - Tim Lloyd

0

下面是重新抛出现有异常对象的两个真实世界应用:

反射

通过反射调用代码时,抛出的任何异常都将包装在 TargetInvocationException 中。但您可能想要/需要处理反射“屏障”之外的异常。在这种情况下,包装将使异常处理代码变得复杂。相反,您可以取消包装原始异常并重新抛出。

如果您使用的是 .NET 4.5+,则可以使用 ExceptionDispatchInfo 以保留原始堆栈跟踪:

MethodInfo someMethod = ...
try
{
    someMethod.Invoke();
}
catch (TargetInvocationException e)
{
    ExceptionDispatchInfo.Capture(e.InnerException).Throw();
}

任务 API

在 .NET 任务 API 中,执行任务时抛出的异常会被包装在 AggregateException 中。当使用 async/await 时,框架会在内部执行解包操作,以便抛出原始异常而不是包装的 AggregateException。

除了 BCL 代码需要在自动解包时重新抛出现有异常之外,还有一些情况下您无法使用 async/await,因此需要手动解包和重新抛出。关于这个实际例子,请查看 Stephen Cleary 的优秀项目 AsyncExTaskExtensions.WaitAndUnwrapExceptionAsyncContext 实现中的内部用途。

-3
您可能希望记录异常,先记录它,然后将其作为“throw ex”抛到上层。您还可能想在此层中进行日志记录,但这可能与您收到异常的层无关,或者您可能不需要异常的详细信息,因为您已经记录了它。
假设您有:
Public int GetAllCustomers()
{
     try
     {
          return data.GetAllCustomers()
     }
     catch()
     {
         Logger.log("Error calling GetAllCustomers");
     }
}
     //data
Public static int GetAllCustomers()
{
     try
     {
          //db operations...
     }
     catch(Exception ex)
     {
          Logger.log(ex.Stacktrace);
          throw ex;
     }
}

8
那里应该使用throw;。请再读一遍问题,因为你没有回答它。 - jgauffin
5
实际上你是对的,你回答了这个问题。虽然我从来不会因为记录了信息就隐藏它 :) - jgauffin
8
我认为在这种情况下,我宁愿抛出一个不同的异常或不记录日志。例如,在设计库时,我会在与公共界面的边界处捕获并记录日志,然后抛出一个MyLibraryException异常。我只是没有看到这种情况的实际用途,但也许我没有看到整个大局。 - Michael Stum
@Michael ~ 同意,但那是你选择使用的设计模式。 - Pabuc
5
我也可以提出一些毫无意义的使用throw ex;的用法,但这并没有真正回答问题。当我回答你的评论时,我确实取消了我的反对票。但是由于你自己说过不会像答案中那样做,所以我重新添加了它。因此,这并没有真正回答问题。 - jgauffin
显示剩余2条评论

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