如何在不重置堆栈跟踪的情况下抛出异常?

32

这是对 “throw”和“throw ex”有区别吗 的跟进问题。

是否有一种方法可以提取一个新的错误处理方法而不重置堆栈跟踪?

[编辑] 我将尝试使用“内部方法”和Earwicker提供的另一个答案,看看哪个可以更好地标记一个答案。


你是什么意思说“提取一个新的错误处理方法”? - Marc Gravell
我实际上更感兴趣的是如何在提取的错误处理方法中模拟“throw;”。 - dance2die
你不能在提取的方法内部执行 "throw;",因为该方法也可能被调用而不是来自 catch 块(例如使用 "new Exception()" 作为参数调用它)。实现这一点的唯一方法可能是使用返回代码,就像我在答案中建议的那样。 - Lucero
5个回答

57

在.NET Framework 4.5中,现在有一个ExceptionDispatchInfo,支持这种情况。它允许捕获完整的异常并从其他地方重新抛出它,而不会覆盖包含的堆栈跟踪。

由于评论要求提供代码示例

using System.Runtime.ExceptionServices;

class Test
{
    private ExceptionDispatchInfo _exInfo;

    public void DeleteNoThrow(string path)
    {
        try { File.Delete(path); }
        catch(IOException ex)
        {
            // Capture exception (including stack trace) for later rethrow.
            _exInfo = ExceptionDispatchInfo.Capture(ex);
        }
    }

    public Exception GetFailure()
    {
        // You can access the captured exception without rethrowing.
        return _exInfo != null ? _exInfo.SourceException : null;
    }

    public void ThrowIfFailed()
    {
        // This will rethrow the exception including the stack trace of the
        // original DeleteNoThrow call.
        _exInfo.Throw();

        // Contrast with 'throw GetFailure()' which rethrows the exception but
        // overwrites the stack trace to the current caller of ThrowIfFailed.
    }
}

6
虽然现有的方法和MSDN文档已经很明显了,但我还是添加了一些示例代码,以便不必跳转到MSDN即可给出概述。 - Zarat
1
这正是我想要的,但不幸的是我的当前项目不支持4.5。但因为让我了解到这个很棒的新功能,所以点个赞。 - AnorZaken
我可以把这个放在新的异常类中吗? - Murat Can OĞUZHAN
@MuratCanOĞUZHAN 这些是静态辅助方法,已经对每个人都可用,因此将其放在异常本身上的用处很小;但如果您想要更好的可发现性,可以将它们制作为扩展方法。 - Zarat

52

是的,这就是 InnerException 属性的作用。

catch(Exception ex)
{
    throw new YourExceptionClass("message", ex);
}

这将允许您添加自己的逻辑,然后抛出自己的异常类。YourExceptionClass实例的StackTrace将来自于此代码块内部,但InnerException将是您捕获的异常,带有它之前所拥有的StackTrace。


4
实际上,这种方法的缺点在于许多错误日志仅包含顶部异常的详细信息。这个方法可以避免这种情况。 - Drew Noakes

34

不确定你是否指的是这个,但是我在你的另一个问题中提供了建议来解决这个问题。

如果你的处理程序返回一个布尔值表示异常是否被处理,你可以在catch语句中使用它:

catch (Exception ex) {
  if (!HandleException(ex)) {
    throw;
  }
}

+标记为答案:似乎是最容易追踪错误源并且易于阅读的。顺便说一下,我的“HandleException”方法被命名为“TryHandleRecoverableException”。 - dance2die
如果错误在与 catch 块相同的堆栈级别上抛出(例如,NullReferenceException 将出错),那么这仍然会破坏堆栈跟踪。请参见 https://dev59.com/0nVD5IYBdhLWcg3wTZxm#11284872。 - NH.

5

您不希望使用原始堆栈跟踪创建新的异常。这是误导性的,因为该堆栈跟踪并没有创建新的异常。

但是,您可以将原始异常作为“InnerException”放入新的异常中。这样做是否符合您的要求?


1
我想在提取的错误处理方法中模拟“throw;”。 - dance2die

3
你是否捕获了需要进一步筛选的异常,以便更仔细地处理它们,这样您可以改变主意,决定不处理它们,然后重新抛出它们?
如果您想要非常小心谨慎,那么这并不是一个好主意。最好的方法是从一开始就不要捕获异常。原因在于,给定的try/catch处理程序不应该决定运行嵌套的finally块来处理它没有预期到的异常。例如,如果有NullReferenceException,继续执行任何代码可能会导致另一个类似的异常被抛出,这可能是非常糟糕的。而finally块只是像任何其他代码一样的代码。一旦第一次捕获异常,位于try/catch下面的堆栈上的任何finally块都将被执行,这时为时已晚,可能会生成另一个异常,这意味着原始异常已丢失。
这意味着(在C#中),您必须小心编写单独的catch处理程序以捕获所有要捕获的异常类型。它还意味着您只能按异常类型进行筛选。这有时是非常难遵循的建议。

在其他方式中,过滤异常应该是可行的,但在C#中不行。然而,在VB.NET中是可行的,并且BCL本身利用这一点,通过使用少量VB.NET代码以更方便的方式过滤异常。

这里有一个来自CLR团队博客的详细解释和VB.NET代码示例。

这是我的个人见解。


“finally”代码总是会执行,无论是否发生异常以及您如何处理异常。我看不出与问题的关系… - Lucero
@Rich - 这取决于情况。意外异常意味着程序状态出现了意外情况。如果你愿意冒险并让应用程序在未知状态下运行,那么你可以捕获所有异常。但这并不总是可接受的;你可能担心“未知状态”可能会导致有价值的持久数据损坏(例如删除错误的文件、启动错误的导弹、杀死错误的患者)。因此,有时候让软件崩溃是必要的,以使其更加健壮!这是一种包含完全未知范围的潜在灾难的方式。 - Daniel Earwicker
@Daniel,我认为你过于悲观了。在像C#这样的托管语言中,即使是最意外的异常也保证不会破坏上层任何内容。有很多情况下捕获所有异常是完全合理的。例如,假设一个Web服务器在渲染页面时出现意外异常:空引用;甚至是内存不足异常(比如用户请求了太多结果)。在这些情况下,您不希望杀死整个服务器进程:最好是用catch all exceptions块包围每个请求。后续请求将正常执行。 - Rich
程序继续在未知状态下艰难前行,稍后它高兴地删除了fileToDeleteLater文件。托管语言极大地减少了程序状态中的自由度,但它们并不能神奇地将其减少到只有可取状态是可访问的。 - Daniel Earwicker
如果您正确使用作用域...也就是说,如果您的代码中没有放置任何状态破坏性错误。假设这是普遍适用的,显然这不是问题!根据定义,错误是指您没有正确执行某些操作的情况。 - Daniel Earwicker
显示剩余10条评论

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