42

我用“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 对此进行了测试。

4

12 回答 12

27

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

如果您从其他方法抛出,throw;将不会像预期的那样删除 for 的条目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()则堆栈跟踪中的条目将消失。同样,这是预期的行为。

于 2010-11-18T17:29:29.553 回答
26

这是可以认为是预期的事情。如果您指定throw ex;,修改堆栈跟踪是通常的情况,FxCop 将通知您堆栈已被修改。如果您 make throw;,不会生成警告,但仍然会修改跟踪。所以不幸的是,现在最好不要抓住前任或把它当作内心的。我认为它应该被视为Windows 影响或类似的东西- 编辑Jeff Richter在他的“CLR via 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 */ }
  }
}
于 2011-01-19T09:16:47.237 回答
20

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

于 2010-11-18T18:50:57.823 回答
10

如何保留 REAL 堆栈跟踪?

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

但那是丑陋的......更长......让你选择正确的异常抛出......

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

您可能还想简单地放置一个 catch 以便您可以在 catch 中设置断点,但 Visual Studio 调试器有足够的选项使这种做法变得不必要,请尝试使用第一次机会异常或条件断点。

于 2011-01-19T09:57:57.483 回答
7

编辑/替换

行为实际上是不同的,但微妙地如此。至于为什么行为不同,我需要听从 CLR 专家的意见。

编辑:AlexD 的回答似乎表明这是设计使然。

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

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;使用,则调用堆栈为(行号替换为代码):

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

如果throw ex;使用,则调用堆栈为:

at Main():line (throw ex;)

如果未捕获到异常,则调用堆栈为:

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

在 .NET 4 / VS 2010 中测试

于 2011-01-19T09:13:38.233 回答
5

这里有一个重复的问题。

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

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

结论:避免使用 throw;并将您的异常包装在重新抛出的新异常中-这并不难看,这是最佳实践。

于 2011-01-19T10:12:34.633 回答
5

您可以使用保留堆栈跟踪

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

输出将类似于:

测试应用前
测试内在ex
   在 D:\Projects\TestApp\TestApp\Program.cs:line 19 中的 TestApp.Program.CallAndThrow()
   在 D:\Projects\TestApp\TestApp\Program.cs:line 30 中的 TestApp.Program.Main(String[] args)
--- 从先前抛出异常的位置结束堆栈跟踪 ---
   在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   在 D:\Projects\TestApp\TestApp\Program.cs:line 38 中的 TestApp.Program.Main(String[] args)
--- 从先前抛出异常的位置结束堆栈跟踪 ---
   在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   在 D:\Projects\TestApp\TestApp\Program.cs:line 47 中的 TestApp.Program.Main(String[] args)
于 2016-07-12T11:43:25.430 回答
3

OK,.NET Framework好像有个bug,如果抛出异常,在同一个方法中重新抛出,原来的行号就丢失了(会是方法的最后一行)。

幸运的是,一个名叫 Fabrice MARGUERIE 的聪明人找到了解决这个错误的方法。下面是我的版本,您可以在这个 .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;
}

现在像往常一样捕获异常,而不是 throw;只需调用此方法,瞧,原始行号将被保留!

于 2014-02-05T10:43:54.657 回答
2

不确定这是否是设计使然,但我认为它一直都是这样。

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

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

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

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

于 2010-11-18T18:27:38.470 回答
1

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

5分钟示例代码:

菜单File -> New Project,放置三个按钮,分别调用如下代码:

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

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

于 2012-07-25T17:40:52.070 回答
0

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

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

我遇到过几次,这可能是设计使然,只是没有完整记录。我可以理解他们为什么会这样做,因为重新抛出位置非常有用,如果你的方法足够简单,那么原始来源通常是显而易见的。

正如许多其他人所说,通常最好不要捕获异常,除非您确实必须这样做,并且/或者您将在那时处理它。

有趣的旁注:Visual Studio 2010 甚至不允许我构建问题中提供的代码,因为它在编译时会发现除以零错误。

于 2011-01-19T09:59:51.290 回答
-2

那是因为您Exception第 12 行捕获并在第 15 行重新抛出它,因此堆栈跟踪将其作为现金,即Exception从那里抛出。

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

于 2010-11-18T17:17:46.600 回答