关于c#:什么可以导致重置一个callstack(我正在使用“throw”,而不是“throw ex”)

what can lead throw to reset a callstack (I'm using “throw”, not “throw ex”)

我一直认为"throw"和"throw ex"之间的区别在于,单靠throw并不能重置异常的stacktrace。

不幸的是,这不是我正在经历的行为;下面是一个简单的复制我问题的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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


你应该阅读这篇文章:

  • 重新引发异常并保留完整的调用堆栈跟踪

简而言之,throw通常保留原始抛出异常的堆栈跟踪,但前提是当前堆栈帧(即方法)中没有发生异常。

您使用的方法PreserveStackTrace(如该博客文章所示)保留了原始堆栈跟踪,如下所示:

1
2
3
4
5
6
7
8
9
try
{

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

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

1
2
3
4
5
6
7
8
try
{

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


问题是Windows正在重置堆栈的起始点。clr的行为与预期的一样,这只是主机操作系统异常处理支持的一个限制。问题是每个方法调用只能有一个堆栈帧。

您可以将异常处理例程提取到一个单独的"helper"方法中,该方法可以绕过Windows的SEH所施加的限制,但我认为这不一定是个好主意。

在不丢失堆栈信息的情况下重新引发异常的正确方法是抛出一个新的异常,并将原始捕获的异常作为内部异常包括在内。

很难想象很多情况下你真的需要这样做。如果您不处理异常,只是捕获它来重新引发异常,那么您可能不应该首先捕获它。


正常的rethrow保留堆栈跟踪上的所有内容,但如果当前方法在堆栈跟踪中,则行号将被覆盖。这是令人讨厌的行为。在C中,如果一个人需要在异常情况下做一些事情,但不关心异常是什么,那么他可以使用以下模式:

1
2
3
4
5
6
7
8
9
10
11
  Boolean ok = False;
  try
  {
    do_something();
    ok = True;
  }
  finally
  {
    if (!ok) // An exception occurred!
      handle_exception();
  }

在某些情况下,该模式非常有用;最常见的是一个函数,它应该返回一个新的IDisposable。如果函数不返回,则必须清理一次性对象。请注意,上述"try"块中的任何"return"语句都必须将OK设置为true。

在vb.net中,可以使用功能更好的模式,尽管代码中的一个点有点恶心,但模式:

1
2
3
4
5
6
7
8
9
10
11
  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语句也可能正常完成。在这种情况下应该发生什么还不清楚,但有一件事是很清楚的,最终的语句不应该像一个异常正在等待处理一样工作。清除PendingException会使异常消失,代码的行为就好像从未发生过一样。另一种选择是包装并重新引发一个已知已发生的异常,因为这种情况几乎肯定表明内部异常处理代码有问题。