重新抛出不正确的堆栈跟踪
我用“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
出现。