什么情况会导致使用"throw"而不是"throw ex"重置调用堆栈?

27
我一直认为"throw"和"throw ex"之间的区别在于,仅使用"throw"无法重置异常的堆栈跟踪。这里有相关讨论。
不幸的是,我的实际情况与此不符;以下是一个简单的示例,可以重现我的问题:
using System;
using System.Text;

namespace testthrow2
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                try
                {
                    throw new Exception("line 14");
                }
                catch (Exception)
                {
                    throw; // line 18
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());

            }
            Console.ReadLine();
        }
    }
}

我期望这段代码从第14行开始打印调用堆栈;然而,调用堆栈从第18行开始。在这个示例中当然没什么大不了的,但在我的实际应用程序中,丢失初始错误信息是非常痛苦的。
我是否漏掉了一些明显的东西?还有其他方法可以实现我想要的(即重新抛出异常而不丢失堆栈信息)?
我正在使用 .net 3.5
3个回答

37

你应该阅读这篇文章:

简而言之,throw 通常 会保留原始抛出异常的堆栈跟踪,但仅当异常未发生在当前堆栈帧(即方法)中时才会保留。

有一种叫做 PreserveStackTrace 的方法(在那篇博客文章中介绍),可以使用它来保留原始的堆栈跟踪,就像这样:

try
{

}
catch (Exception ex)
{
    PreserveStackTrace(ex);
    throw;
}

但我的常规解决方案要么是不捕获和重新抛出这样的异常(除非绝对必要),要么就是始终使用 InnerException 属性抛出新的异常来传播原始异常:

try
{

}
catch (Exception ex)
{
     throw new Exception("Error doing foo", ex);
}

谢谢,你提供的链接非常有用,直接调用 InternalPreserveStackTrace 让我解决了我的问题! - Brann
2
如果上面的链接失效,或者有人不想遵循它,这是您需要执行的操作来保留异常堆栈:typeof(Exception).GetMethod("InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(ex, null); - Derek Kalweit

11
问题在于Windows正在重置堆栈的起始点。CLR的行为符合预期,这只是主机操作系统异常处理支持的限制。问题在于每个方法调用只能有一个堆栈帧。
你可以将异常处理例程提取到单独的“helper”方法中,以绕过Windows的SEH所施加的限制,但我不认为这是一个好主意。
重新抛出异常而不丢失堆栈信息的正确方法是抛出一个新异常,并将原始捕获的异常包含在内部异常中。
很难想象有很多情况下你真正需要这样做。如果你不是处理异常,而只是捕获它来重新抛出它,那么你可能不应该首先捕获它。

1
@Brann:嗯,它们仍然不同。throw ex会重置堆栈跟踪,而throw则不会。你在这里设置的问题是一个边缘情况。你在同一个方法中有异常处理程序,而Windows每个方法调用只支持一个堆栈帧。throwthrow ex之间的区别在CLR层面上;正如我所解释的那样,在这里它是正确工作的。你只是碰到了主机操作系统的限制。 - Cody Gray
@Brann:是的,如果你将代码分割成一个单独的帮助方法,它会"运行"。但我不会仅为了修复异常处理例程而这样做。 - Cody Gray
@Brann:我不确定我在回答中漏掉了哪一部分。你是指通过将它们分成两个不同的方法来拼凑东西吗?我想我可以加上这一点,但是就像我说的,我认为这并不一定是一个好主意。 - Cody Gray
@Brann:我认为我的回答的第一段已经充分区分了CLR的异常处理和主机操作系统(Windows)的异常处理。这就是传统智慧的问题所在。我已经建议了如何以正确的方式重新抛出异常,即将它们作为新异常的内部异常捆绑起来。使用throw并不是一个旨在取代它的魔法弹药。大多数情况下,它最终都是无意义的,因为如果你可以简单地重新抛出原始异常,你本来就不应该首先捕获它。 - Cody Gray
@Brann:当异常发生时会触发特殊代码,但不是真正的捕获异常的想法是很好的。在我的答案中,我提供了一种在C#中实现它的方法,以及在vb.net中执行更强大的方法(即使您使用C#编程,如果需要捕获未处理异常而又不想要“catch”它,则可能有用于包装vb.net例程)。 - supercat
显示剩余6条评论

3
通常的重新抛出异常会保留堆栈跟踪中的所有内容,除非当前方法在堆栈跟踪中,行号将被覆盖。这是令人烦恼的行为。在C#中,如果需要在异常情况下做一些事情但不关心异常是什么,则可以使用以下模式:
Boolean ok = False; try { do_something(); ok = True; } finally { if (!ok) // An exception occurred! handle_exception(); }
在许多情况下,该模式非常有用;最常见的是一个函数应返回一个新的IDisposable。如果函数不会返回,则必须清理一次性对象。请注意,上述“try”块中的任何“return”语句都必须将ok设置为true。
在VB.NET中,可以使用功能上更好的模式,尽管代码中的一个地方有些棘手,该模式如下:
Dim PendingException As Exception = Nothing; Try Do_Something PendingException = Nothing ' See note Catch Ex As Exception When CopyFirstParameterToSecondAndReturnFalse(Ex, PendingException ) Throw ' Will never execute, since above will return false Finally If PendingException IsNot Nothing Then .. Handle exception EndIf End Try
应以明显的方式实现长命名函数。此模式的优点是使异常对代码可用。虽然在处理但不捕获的情况下通常不需要,但有一种情况可能非常有用:如果清理例程引发异常。通常,如果清理例程引发异常,则任何待处理的异常都将丢失。但是,使用上述模式,可以将待处理异常包装在清理异常中。
上面的代码一个有趣的问题是:异常可以到达“Catch When”,但Try语句可能会正常完成。这种情况下应该发生什么并不清楚,但有一件明确的事情是,Finally语句不应像有异常未处理一样执行。清除PendingException将使得如果异常消失,代码将表现得好像从未发生过异常。另一种选择是包装和重新抛出已知发生的异常,因为那种情况几乎肯定表示内部异常处理代码存在问题。

我的第一反应与您的第一个示例相同,但很难想象有一个实现了 IDisposable 的对象,而您不能将整个对象包装在 using 块中。这里还有几个其他问题,可以得出结论,在 using 块内部返回没有任何问题。至于您的第二个示例,呃!它确实看起来“聪明”,但是那是那种对自己不利的事情之一。我无法想象被迫维护像那样的代码。将其作为内部异常包装更清晰。 - Cody Gray
@Cody Gray:如何在“using”块中有用地包装构造函数?如果对象将在不返回给调用者的情况下超出作用域,则必须进行Dispose,但是如果对象将返回给调用者,则不能这样做。 “using”块无法为此类情况提供任何帮助。 - supercat
@Cody Gray:对于捕获待处理异常的可怕代码,我希望有一些更好的语言结构,但实际上没有。 如果需要清理的代码区域引发异常,则很常见导致异常的条件也会导致清理失败。 吞噬清理失败的异常是不好的,但是清理代码抛出破坏先前异常的新异常也是不好的。 最好的方法是使用旧异常作为 InnerException 抛出一个异常,但是如何做到这一点而不使用类似我展示的代码呢? - supercat

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