重新抛出不正确的堆栈跟踪

我用“throw”重新抛出exception,但是堆栈跟踪不正确:

static void Main(string[] args) { try { try { throw new Exception("Test"); //Line 12 } catch (Exception ex) { throw; //Line 15 } } catch (Exception ex) { System.Diagnostics.Debug.Write(ex.ToString()); } Console.ReadKey(); } 

正确的堆栈跟踪应该是:

 System.Exception: Test at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 12 

但是我得到:

 System.Exception: Test at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 15 

但第15行是“投掷”的位置。 我用.NET 3.5testing了这个。

在同一个方法中抛出两次可能是一种特殊情况 – 我无法创build一个堆栈跟踪,其中同一个方法中的不同行跟在另一个之后。 正如这个词所说,一个“堆栈跟踪”显示了一个exception遍历的堆栈帧。 每个方法调用只有一个堆栈框架!

如果你从另一个方法throw;throw; 不会像预期的那样删除Foo()的条目:

  static void Main(string[] args) { try { Rethrower(); } catch (Exception ex) { Console.Write(ex.ToString()); } Console.ReadKey(); } static void Rethrower() { try { Foo(); } catch (Exception ex) { throw; } } static void Foo() { throw new Exception("Test"); } 

如果修改Rethrower()并replacethrow; throw ex; 堆栈跟踪中的Foo()条目消失。 再次,这是预期的行为。

这是可以按预期考虑的事情。 如果指定了throw ex;通常情况下修改堆栈跟踪throw ex; ,FxCop会通知你堆栈被修改。 如果你throw; ,不会产生警告,但仍会修改跟踪。 所以不幸的是现在最好不要抓住前者或者把它当作内在的东西。 我认为这应该被视为Windows的影响或像这样的编辑Jeff Richter在他的“CLR via C#”中更详细地描述了这种情况:

以下代码将引发它捕获的相同exception对象,并导致CLR重置其exception的起点:

 private void SomeMethod() { try { ... } catch (Exception e) { ... throw e; // CLR thinks this is where exception originated. // FxCop reports this as an error } } 

相反,如果您通过使用throw关键字本身重新抛出一个exception对象,CLR不会重置堆栈的起点。 以下代码重新引发它捕获的同一个exception对象,导致CLR不重置exception的起点:

 private void SomeMethod() { try { ... } catch (Exception e) { ... throw; // This has no effect on where the CLR thinks the exception // originated. FxCop does NOT report this as an error } } 

实际上,这两个代码片段之间的唯一区别是CLR认为是抛出exception的原始位置。 不幸的是,当你抛出或重新抛出exception时,Windows会重置堆栈的起点。 因此,如果exception变得未处理,即使CLR知道引发原始exception的堆栈位置,报告给Windows错误报告的堆栈位置也是最后一次抛出或重新抛出的位置。 这是不幸的,因为它使debugging失败的应用程序更加困难。 一些开发人员发现,这是不能容忍的,他们已经select了不同的方式来实现他们的代码,以确保堆栈跟踪真正反映了最初抛出exception的位置:

 private void SomeMethod() { Boolean trySucceeds = false; try { ... trySucceeds = true; } finally { if (!trySucceeds) { /* catch code goes in here */ } } } 

这是CLR的Windows版本中的一个众所周知的限制。 它使用Windows内置的exception处理支持(SEH)。 问题是,它是基于堆栈帧,并且一个方法只有一个堆栈帧。 您可以通过将内部try / catch块移动到另一个辅助方法来轻松解决问题,从而创build另一个堆栈框架。 此限制的另一个后果是JIT编译器不会内联包含try语句的任何方法。

我怎样才能保留真正的stacktrace?

您抛出一个新的exception,并将原始exception作为内部exception。

但这是丑陋的…更长…让你select严重的例外抛….

你对这个丑陋的看法是错误的,但对另外两点则是正确的。 经验法则是:除非你打算做一些事情,比如包装,修改,吞下或logging,否则不要抓住。 如果你决定catch ,然后再throw ,确保你正在做一些事情,否则就让它冒出来。

你也可能会试图简单地把一个catch语句放在catch语句中,但是Visual Studiodebugging器有足够的选项来使这个练习变得不必要,可以尝试使用一次机会exception或条件断点。

编辑/replace

行为实际上是不同的,但是却是如此。 至于为什么行为如果不同,我需要推迟到CLR专家。

编辑: AlexD的答案似乎表明,这是由devise。

在捕获它的同一个方法中抛出exception会让情况混淆一点,所以让我们从另一个方法抛出exception:

 class Program { static void Main(string[] args) { try { Throw(); } catch (Exception ex) { throw ex; } } public static void Throw() { int a = 0; int b = 10 / a; } } 

如果throw; 被使用,callstack是(行号被代码replace):

 at Throw():line (int b = 10 / a;) at Main():line (throw;) // This has been modified 

如果throw ex; 被使用,这个callstack是:

 at Main():line (throw ex;) 

如果exception没有被捕获,那么callstack是:

 at Throw():line (int b = 10 / a;) at Main():line (Throw()) 

在.NET 4 / VS 2010中testing

这里有一个重复的问题。

据我了解 – 扔; 被编译成“rethrow”MSIL指令 ,并修改栈跟踪的最后一帧。

我期望它保持原始的堆栈跟踪,并添加它已经被重新抛出的行,但显然每个方法调用只能有一个堆栈帧

结论:避免使用投掷; 并把你的例外包装在一个新的重新投掷 – 这不是丑陋的,这是最好的做法。

您可以使用保留堆栈跟踪

 ExceptionDispatchInfo.Capture(ex); 

这里是代码示例:

  static void CallAndThrow() { throw new ApplicationException("Test app ex", new Exception("Test inner ex")); } static void Main(string[] args) { try { try { try { CallAndThrow(); } catch (Exception ex) { var dispatchException = ExceptionDispatchInfo.Capture(ex); // rollback tran, etc dispatchException.Throw(); } } catch (Exception ex) { var dispatchException = ExceptionDispatchInfo.Capture(ex); // other rollbacks dispatchException.Throw(); } } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.InnerException.Message); Console.WriteLine(ex.StackTrace); } Console.ReadLine(); } 

输出结果如下所示:

 testing应用程序前
testing内部
   在TestApp.Program.CallAndThrow()在D:\ Projects \ TestApp \ TestApp \ Program.cs中:第19行
   在TestApp.Program.Main(String []参数)在D:\ Projects \ TestApp \ TestApp \ Program.cs中:第30行
 ---抛出exception的前一个位置的堆栈跟踪结束---
   在System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   在TestApp.Program.Main(String []参数)在D:\ Projects \ TestApp \ TestApp \ Program.cs中:第38行
 ---抛出exception的前一个位置的堆栈跟踪结束---
   在System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   在TestApp.Program.Main(String []参数)在D:\ Projects \ TestApp \ TestApp \ Program.cs中:第47行 

好的,.NET框架中似乎有一个错误,如果抛出一个exception,并用相同的方法重新抛出exception,原始行号会丢失(这将是方法的最后一行)。

幸运的是,一个名叫Fabrice MARGUERIE的聪明人find了解决这个问题的方法。 下面是我的版本,你可以在这个.NET小提琴中testing。

 private static void RethrowExceptionButPreserveStackTrace(Exception exception) { System.Reflection.MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); preserveStackTrace.Invoke(exception, null); throw exception; } 

现在像通常那样捕捉exception,而不是抛出; 只是调用这个方法,瞧,原来的行号会被保留!

不知道这是否是由devise,但我认为它一直是这样的。

如果原始抛出新的exception是在一个单独的方法中,那么抛出的结果应该有原始的方法名称和行号,然后是重新抛出exception的main行号。

如果使用throw ex,那么结果将只是exception重新抛出的主线。

换句话说,throw ex丢失了所有的堆栈跟踪,而throw则保留了堆栈跟踪历史 (即较低级别的方法的细节)。 但是,如果您的exception是通过与重新抛出相同的方法生成的,则可能会丢失一些信息。

NB。 如果你写一个非常简单的小testing程序,框架有时可以优化事物,并将方法改为内联代码,这意味着结果可能与“真实”程序不同。

你想要正确的行号吗? 每个方法只使用一个 try / catch。 在系统中,好吧…只是在UI层,而不是逻辑或数据访问,这是非常烦人的,因为如果你需要数据库事务,那么它们不应该在UI层中,而且你也不会正确的行号,但如果你不需要他们,不要重新抛出,也不要在捕捉exception…

5分钟样本代码:

菜单文件 – > 新build项目 ,放置三个button,并在每个button中调用以下代码:

 private void button1_Click(object sender, EventArgs e) { try { Class1.testWithoutTC(); } catch (Exception ex) { MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException); } } private void button2_Click(object sender, EventArgs e) { try { Class1.testWithTC1(); } catch (Exception ex) { MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException); } } private void button3_Click(object sender, EventArgs e) { try { Class1.testWithTC2(); } catch (Exception ex) { MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException); } } 

现在,创build一个新的类:

 class Class1 { public int a; public static void testWithoutTC() { Class1 obj = null; obj.a = 1; } public static void testWithTC1() { try { Class1 obj = null; obj.a = 1; } catch { throw; } } public static void testWithTC2() { try { Class1 obj = null; obj.a = 1; } catch (Exception ex) { throw ex; } } } 

跑…第一个button很漂亮!

我认为这不是堆栈跟踪更改的一种情况,更多的是确定堆栈跟踪的行号的方式。 在Visual Studio 2010中尝试一下,其行为与您对MSDN文档的期望类似:“throw ex;” 从这个语句重新构build堆栈跟踪,“扔”; 将堆栈跟踪保持原样,除了重新抛出exception的地方,行号是重新抛出的位置,而不是exception通过的调用。

所以用“扔”; 方法调用树保持不变,但行号可能会更改。

我已经遇到了这几次,这可能是devise,并没有完全logging。 我可以理解为什么他们可能做到了这一点,因为重新select位置是非常有用的知道,如果你的方法很简单,原始来源通常是显而易见的。

正如许多其他人所说的那样,通常最好不要抓住例外情况,除非您真的必须和/或您要在那个时候处理这​​个例外情况。

有趣的一面是:Visual Studio 2010甚至不会让我编译问题中提供的代码,因为它在编译时拾取除零错误。

那是因为你捕捉到了第12行Exception ,并在第15行重新抛出它,所以堆栈跟踪将其作为现金,exception从那里抛出。

为了更好地处理exception,你应该简单地使用try...finally ,并让未处理的Exception出现。