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

时间:2010-11-18 17:15:04

标签: c# stack-trace throw rethrow

我用“throw;”重新抛出异常,但堆栈跟踪不正确:

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行是“throw;”的位置。我用.NET 3.5进行了测试。

12 个答案:

答案 0 :(得分:27)

在同一方法中投掷两次可能是一种特殊情况 - 我无法创建堆栈跟踪,其中同一方法中的不同行彼此跟随。正如单词所说,“堆栈跟踪”向您显示异常遍历的堆栈帧。每个方法调用只有一个堆栈帧!

如果您使用其他方法,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()并将throw;替换为throw ex;,则堆栈跟踪中的Foo()条目将消失。再次,这是预期的行为。

答案 1 :(得分:24)

这可以被认为是预期的。 如果指定throw ex;,则通常会修改堆栈跟踪,FxCop将通知您堆栈已被修改。如果您生成throw;,则不会生成警告,但仍会修改跟踪。 所以不幸的是,现在最好不要抓住前任或将其作为内部投掷。 我认为它应该被视为 Windows影响或者像 - 编辑那样。 Jeff Richter 在他的“CLR中通过C#”更详细地描述了这种情况

  

以下代码抛出相同的内容   它捕获的异常对象和   导致CLR重置其启动   异常点:

private void SomeMethod() {
  try { ... }
  catch (Exception e) {
    ...
    throw e; // CLR thinks this is where exception originated.
    // FxCop reports this as an error
  }
}
     相反,如果你重新抛出一个   使用throw的异常对象   关键字本身,CLR没有   重置堆栈的起点。该   下面的代码重新抛出相同的内容   它捕获的异常对象,   导致CLR无法重置它   例外的起点:

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认为是原始位置   抛出异常的地方。   不幸的是,当你扔或者   Windows会重新抛出异常   重置堆栈的起点。所以   如果异常变得无法处理,   报告的堆栈位置   到Windows错误报告是   最后一次投掷的位置或   重新抛出,即使CLR知道   原始堆栈的位置   异常被抛出。这是   不幸的是因为它会调试   失败的应用程序   场难得多。一些   开发人员已经发现了这一点   无法忍受他们选择了一个   实现其代码的不同方式   确保堆栈跟踪真正   反映了一个地方   异常最初抛出:

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

答案 2 :(得分:19)

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

答案 3 :(得分:10)

  

如何保留REAL堆栈跟踪?

您抛出一个新异常,并将原始异常包含为内部异常。

  

但是这很丑......更长......让你选择抛出的例外......

你错了丑陋,但对其他两点是对的。经验法则是:除非你打算用它做什么,否则不要抓住它,比如包装它,修改它,吞下它或记录它。如果您再次决定catch然后throw,请确保您正在使用它,否则只是让它冒泡。

您可能也很想放置一个catch,因此您可以在catch中使用断点,但Visual Studio调试器有足够的选项可以使该练习不必要,请尝试使用第一次机会异常或条件断点。

答案 4 :(得分:7)

修改/替换

这种行为实际上是不同的,但却是极端的。至于为什么行为如果不同,我需要推荐给CLR专家。

编辑:AlexD's answer似乎表明这是设计上的。

在捕获它的同一方法中抛出异常会使情况稍微混淆,所以让我们从另一个方法抛出异常:

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(行号被代码替换):

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

如果使用throw ex;,则callstack为:

at Main():line (throw ex;)

如果未捕获异常,则callstack为:

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

在.NET 4 / VS 2010中测试

答案 5 :(得分:5)

有一个重复的问题here

据我了解 - 扔;被编译成'rethrow' MSIL instruction并且它修改了堆栈跟踪的最后一帧。

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

结论:避免使用throw;并且在重新投掷时将你的异常换成新的 - 这不是丑陋的,这是最好的做法。

答案 6 :(得分:4)

您可以使用

保留堆栈跟踪
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();
    }

输出类似于:

Test app ex
Test inner ex
   at TestApp.Program.CallAndThrow() in D:\Projects\TestApp\TestApp\Program.cs:line 19
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 30
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 38
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 47

答案 7 :(得分:3)

好吧,.NET Framework中似乎存在一个错误,如果你抛出一个异常,并在同一个方法中重新抛出它,原始行号就会丢失(它将是该方法的最后一行)。 / p> 幸运的是,一个名叫Fabrice MARGUERIE的聪明人找到了这个bug的a solution。以下是我的版本,您可以在this .NET Fiddle中进行测试。

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;
}

现在通常会捕获异常,而不是抛出;只需调用此方法,瞧,原始行号将被保留!

答案 8 :(得分:2)

不确定这是否符合设计,但我认为一直都是这样。

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

如果你使用throw ex,那么结果将只是main中重新抛出异常的行。

换句话说,抛弃ex strong all 堆栈跟踪,而throw保留堆栈跟踪历史(即低级方法的详细信息)。但是,如果您的异常是通过与重新抛出相同的方法生成的,那么您可能会丢失一些信息。

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

答案 9 :(得分:1)

您想要正确的行号吗?只需使用 一个 try / catch per方法。在系统中,嗯...只是在UI层,而不是在逻辑或数据访问中,这非常烦人,因为如果你需要数据库事务,那么它们不应该在UI层中,你也不会正确的行号,但如果你不需要它们,不要在捕获中重新抛出,也不要没有例外......

5分钟示例代码:

菜单文件 - > 新建项目,放置三个按钮,并在每个按钮中调用以下代码:

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);
    }
}

现在,创建一个新类:

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;
        }
    }
}

运行......第一个按钮很漂亮!

答案 10 :(得分:0)

我认为这不是堆栈跟踪更改的情况,而是更多地与确定堆栈跟踪的行号的方式有关。在Visual Studio 2010中试用它,行为类似于您对MSDN文档的期望:“throw ex;”从这个语句的角度重建堆栈跟踪,“throw;”除了重新抛出异常的地方之外,行号是重新抛出的位置,而不是异常通过的调用。

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

我偶然遇到过这种情况,它可能是设计上的,并没有完整记录。我可以理解为什么他们可能已经这样做,因为重新抛出位置非常有用,如果你的方法很简单,原始来源通常都是显而易见的。

正如许多其他人所说,除非你真的需要,否则通常最好不要捕捉异常,和/或你将在那时处理它。

有趣的注意事项:Visual Studio 2010甚至不会让我构建问题中提供的代码,因为它在编译时选择了零除错误。

答案 11 :(得分:-2)

这是因为您从第12行抓取了Exception并在第15行上重新播放了它,因此Stack Trace将其视为现金,即Exception从那里被抛出。

为了更好地处理异常,您应该只使用try...finally,并让未处理的Exception冒泡。