10

参考网上的很多文档,特别是关于 SO,例如:在 C# 中重新抛出异常的正确方法是什么? "throw e;" 之间应该有区别 和“扔;”。

但是,来自:http ://bartdesmet.net/blogs/bart/archive/2006/03/12/3815.aspx ,

这段代码:

using System;

class Ex
{
   public static void Main()
  {
  //
  // First test rethrowing the caught exception variable.
  //
  Console.WriteLine("First test");
  try
  {
     ThrowWithVariable();
  }
  catch (Exception ex)
  {
     Console.WriteLine(ex.StackTrace);
  }

  //
  // Second test performing a blind rethrow.
  //
  Console.WriteLine("Second test");
  try
  {
     ThrowWithoutVariable();
  }
  catch (Exception ex)
  {
     Console.WriteLine(ex.StackTrace);
  }
}

 private static void BadGuy()
 {
   //
   // Some nasty behavior.
  //
   throw new Exception();
 }

   private static void ThrowWithVariable()
 {
   try
   {
         BadGuy();
   }
  catch (Exception ex)
  {
     throw ex;
  }
}

   private static void ThrowWithoutVariable()
{
  try
  {
     BadGuy();
  }
  catch
  {
     throw;
  }
   }
}

给出以下结果:

$ /cygdrive/c/Windows/Microsoft.NET/Framework/v4.0.30319/csc.exe Test.cs
Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.

$ ./Test.exe
First test
   at Ex.ThrowWithVariable()
   at Ex.Main()
Second test
   at Ex.ThrowWithoutVariable()
   at Ex.Main()

这与博客文章完全矛盾。

使用以下代码获得相同的结果:http: //crazorsharp.blogspot.com/2009/08/rethrowing-exception-without-resetting.html

原始问题:我做错了什么?

更新:与 .Net 3.5 / csc.exe 3.5.30729.4926 的结果相同

总结:你所有的答案都很棒,再次感谢。

所以原因是由于 64 位 JITter 而有效地内联。

我只能选择一个答案,这就是我选择LukeH答案的原因:

  • 他猜到了内联问题,并且可能与我的 64 位架构有关,

  • 他提供了 NoInlining 标志,这是避免这种行为的最简单方法。

然而这个问题现在引发了另一个问题:这种行为是否符合所有 .Net 规范:CLR 规范和 C# 编程语言规范?

更新:这种优化似乎符合:Throw VS rethrow:相同的结果?(感谢0xA3

在此先感谢您的帮助。

4

5 回答 5

4

我无法复制这个问题——使用 .NET 3.5(32 位)给了我在 Bart 的文章中描述的相同结果。

我的猜测是 .NET 4 编译器/抖动——或者如果这也发生在 3.5 下,它可能是 64 位编译器/抖动——正在将该BadGuy方法内联到调用方法中。尝试添加以下MethodImpl属性BadGuy,看看是否有任何不同:

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
private static void BadGuy()
{
    //
    // Some nasty behavior.
    //
    throw new Exception();
}
于 2010-08-23T22:45:23.487 回答
3

我已经尝试自己运行此代码,并且调试版本按预期工作,但我在发布版本中得到了与您相同的结果。

我怀疑发生的事情是编译器内联只是用 throw 替换了 BadGuy() 调用,new Exception();因为这是 BadGuy() 中的唯一语句。

如果您在项目属性 -> 构建屏幕中关闭“优化代码”选项,则发布和调试构建都会产生相同的结果,即在堆栈跟踪顶部显示 BadGuy()。

于 2010-08-23T22:37:41.523 回答
3

似乎 JIT 优化器在这里做了一些工作。如您所见,当您运行 Debug 构建时,第二种情况下的调用堆栈与第一种情况下的不同。但是,在 Release 版本中,由于优化,两个调用堆栈是相同的。

要查看这与抖动有关,您可以使用MethodImplAttribute 属性装饰方法:

[MethodImpl(MethodImplOptions.NoOptimization)]
private static void ThrowWithoutVariable()
{
    try
    {
        BadGuy();
    }
    catch
    {
        throw;
    }
}

ThrowWithoutVariable请注意,对于和,IL 仍然不同ThrowWithVariable

.method private hidebysig static void  ThrowWithVariable() cil managed
{
  // Code size       11 (0xb)
  .maxstack  1
  .locals init ([0] class [mscorlib]System.Exception ex)
  .try
  {
    IL_0000:  call       void Ex::BadGuy()
    IL_0005:  leave.s    IL_000a
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_0007:  stloc.0
    IL_0008:  ldloc.0
    IL_0009:  throw
  }  // end handler
  IL_000a:  ret
} // end of method Ex::ThrowWithVariable

.method private hidebysig static void  ThrowWithoutVariable() cil managed
{
  // Code size       11 (0xb)
  .maxstack  1
  .try
  {
    IL_0000:  call       void Ex::BadGuy()
    IL_0005:  leave.s    IL_000a
  }  // end .try
  catch [mscorlib]System.Object 
  {
    IL_0007:  pop
    IL_0008:  rethrow
  }  // end handler
  IL_000a:  ret
} // end of method Ex::ThrowWithoutVariable

更新以回答您的后续问题这是否符合 CLI 规范

事实上它是兼容的,即允许 JIT 编译器启用重要的优化。附件 F第 52 页上的声明(我强调):

一些 CIL 指令执行隐式运行时检查,以确保内存和类型安全。最初,CLI 保证异常是精确的,这意味着在抛出异常时会保留程序状态。但是,对隐式检查强制执行精确的异常使得一些重要的优化实际上无法应用。 程序员现在可以通过自定义属性声明一个方法是“宽松的”,这表示隐式运行时检查引起的异常不需要精确。

宽松的检查保留了可验证性(通过保留内存和类型安全),同时允许对指令进行重新排序的优化。特别是,它支持以下优化:

  • 提升隐式运行时检查出循环。
  • 重新排序循环迭代(例如,矢量化和自动多线程)
  • 交换循环
  • 内联使内联方法至少与等效宏一样快
于 2010-08-23T22:38:12.117 回答
1

使用调试版本,您会更清楚地看到差异。使用调试版本,第一次运行会将位置显示为throw ex行,第二次运行将显示为源自对BadGuy. 显然,“问题”是对 BadGuy 的调用 - 而不是 throw ex 行,您将使用直接throw;声明来追逐更少的鬼魂。

在这么浅的堆栈跟踪中,好处并不那么明显,在非常深的堆栈中,您将通过手动抛出异常而不是使用内置的重新抛出语句来掩盖问题的实际根源并失去一些保真度。

于 2010-08-23T22:36:17.950 回答
0

附带说明一下,我曾经在博客上发现了一个 hack(我已经丢失了参考),它允许您在重新抛出时保留调用堆栈。如果您在一个上下文中捕获异常(例如,在运行异步操作的线程中)并希望在另一个上下文中重新抛出它(例如,在启动异步操作的另一个线程中),这主要是有用的。它利用包含的一些未记录的功能来允许跨远程边界保存堆栈跟踪。

    //This terrible hack makes sure track trace is preserved if exception is re-thrown
    internal static Exception AppendStackTrace(Exception ex)
    {
        //Fool CLR into appending stack trace information when the exception is re-thrown
        var remoteStackTraceString = typeof(Exception).GetField("_remoteStackTraceString",
                                                                 BindingFlags.Instance |
                                                                 BindingFlags.NonPublic);
        if (remoteStackTraceString != null)
            remoteStackTraceString.SetValue(ex, ex.StackTrace + Environment.NewLine);

        return ex;
    }
于 2010-08-23T22:47:16.590 回答