为什么在这种情况下,“throw” 和 “throw ex” 表现相同?

4
我感到惊讶。我曾经认为在catch块中仅使用throw会抛出手头的异常而不会改变堆栈跟踪,但在catch块中使用throw ex会更改堆栈跟踪以显示语句所在位置的异常来源。
以下是两个代码块。我希望它们的输出有细微差别,因为其中一个使用了throw,另一个使用了throw ex,但是两者之间的输出相同,并且导致初始异常的实际源代码行在这两种情况下都丢失了,这对我来说似乎很糟糕。我错了吗? 以下是第一个示例,其表现与我的期望相符:
using System;
                    
public class Program
{
    public static void Main()
    {
        try
        {
            DummyWork();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
        
    private static void DummyWork()
    {
        try
        {
            throw new Exception("dummy");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            throw ex;  // I would expect to lose the information about the inciting line 5 above this one in this case.... and I do.
        }
    }
}

这个第二个例子的行为与第一个完全相同,但我不期望这样:

using System;
                    
public class Program
{
    public static void Main()
    {
        try
        {
            DummyWork();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
        
    private static void DummyWork()
    {
        try
        {
            throw new Exception("dummy");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            throw;  // I would NOT expect to lose the information about the inciting line 5 above this one in this case.... But I do.  Output is identical.
        }
    }
}

更新: 一些评论者说他们无法重现这个问题-这是我的dot fiddle(您需要手动编辑它来在两个版本之间来回切换):https://dotnetfiddle.net/Mj7eK5

更新#2:对于一些要求“相同”输出的评论者。以下是第一个示例的输出:

System.Exception: dummy
   at Program.DummyWork() in d:\Windows\Temp\xoyupngb.0.cs:line 21
System.Exception: dummy
   at Program.DummyWork() in d:\Windows\Temp\xoyupngb.0.cs:line 26
   at Program.Main() in d:\Windows\Temp\xoyupngb.0.cs:line 9

以下是第二个示例的输出结果:

System.Exception: dummy
   at Program.DummyWork() in d:\Windows\Temp\jy4xgqrf.0.cs:line 21
System.Exception: dummy
   at Program.DummyWork() in d:\Windows\Temp\jy4xgqrf.0.cs:line 26
   at Program.Main() in d:\Windows\Temp\jy4xgqrf.0.cs:line 9

撇开微不足道的临时文件差异,两种情况下外部catch(第二个)都缺少初始抛出异常的第21行代码。我期望在第一个例子中使用throw ex而不是在第二个例子中使用throw


如果您想查看差异,请添加 static void SecondLevelDummy() => throw new Exception("dummy"); 并在 DummyWork 中的 try 块内调用 SecondLevelDummy(); - 41686d6564 stands w. Palestine
1
重复的问题:“throw”和“throw ex”之间有区别吗? 引用一下:“throw ex会重置堆栈跟踪(因此您的错误将似乎起源于HandleException)”。在您的情况下,HandleExceptionDummyWork。您看不到区别,因为异常无论如何都来自DummyWork - 41686d6564 stands w. Palestine
1
我对那些无法重现这个问题的人感到困惑。这是我的点代码 - 目前设置为第二个示例,但您可以轻松编辑以将其推向第一个示例,然后来回切换,输出相同..... https://dotnetfiddle.net/Mj7eK5 - Stephan G
1
看起来.NET Framework(4.7.2)和.NET 6的行为不同。后者按预期工作,但前者在throw情况下确实会吞噬堆栈,原因不明。 - Ed'ka
1
是的,看起来这个问题已经在.NET Core 2.1中得到了修复,但没有被移植到.NET Framework。 - Ed'ka
显示剩余11条评论
1个回答

7

注意: 本回答适用于.NET Framework。如果您使用的是.NET Core或.NET 5.0及以上版本,可能会观察到不同的行为,具体情况请参考评论。 我没有在所有版本的.NET Core上进行测试


好的,让我试着解释一下。关于throwthrow ex之间的区别已经在Is there a difference between "throw" and "throw ex"?中解释过了,但我会尝试以更清晰的术语来表达,以适应本问题的叙述。

  • throw ex: 从此点重新抛出异常,并重置堆栈跟踪。

  • throw: 从此点重新抛出异常,并保留堆栈跟踪。

让我们看看问题中的代码:

private static void DummyWork()
{
    try
    {
        throw new Exception("dummy");  // Line 21
    }
    catch (Exception ex)
    {
        throw;  // Line 25
    }
}

在这里,无论我们使用 throw 还是 throw ex ,堆栈跟踪始终如下:

at Program.DummyWork() in ...:line 25
at Program.Main() in ...:line 9

问:为什么是“第25行”?

答:因为throwthrow ex都从该点重新抛出异常。

问:在这种情况下为什么没有差别?

答:因为堆栈跟踪中没有更多的堆栈帧需要重置。

问:我们如何看到差异?

好吧,让我们再添加一个级别以生成另一个堆栈帧。 代码应该像这样:

private static void DummyWork()
{
    try
    {
        MoreDummyWork();  // Line 21
    }
    catch (Exception ex)
    {
        throw;  //Line 25
    }
}

private static void MoreDummyWork() 
{
    throw new Exception("dummy");  // Line 31
}

在这里,我们可以清楚地看到区别。如果我们使用 throw,那么堆栈跟踪如下:

at Program.MoreDummyWork() in ...:line 31
at Program.DummyWork() in ...:line 25
at Program.Main() in ...:line 9

但是如果我们使用 throw ex,堆栈跟踪将变成:

at Program.DummyWork() in ...:line 25
at Program.Main() in ...:line 9

Q:好的,你说两者都会从那个点抛出异常。如果我想保留原始行号怎么办?

A:在这种情况下,您可以使用ExceptionDispatchInfo.Capture(ex).Throw();,如在C#中如何重新抛出InnerException而不丢失堆栈跟踪?中所述:


虽然这些都是真的,但我不知道它是否是一个完整的答案:在不同版本的.NET中(比如.NET 6),相同的代码将保持原始堆栈跟踪不变,显示异常是在第21行抛出的,就像OP所期望的那样。 - StriplingWarrior
@StriplingWarrior 这并不是普遍适用的,因为在.NET 6中可以通过[System.Diagnostics.StackTraceHidden]来抑制该行为,这可能是你所指的。据我所知,在.NET 6中,默认行为是相同的。 - David L
这是非常棒的信息,我会将其标记为答案。对我来说,ExceptionDispatchInfo.Capture(ex).Throw()效果很好,因为没有一般性的解决方案可以确保意外异常会在不同的方法中被抛出。 - Stephan G
@DavidL: 在我的测试中,StackTraceHidden 属性从 StackTrace 中删除了整个 DummyWork 方法。但如果没有该属性,我会看到堆栈跟踪显示原始 throw new ... 语句的行号。我认为这就是为什么许多评论者难以重现此问题的原因:行为在 .NET Framework 和 .NET Core 之间有所不同。 - StriplingWarrior
@StriplingWarrior 哦,我明白你的观点了。这很有趣,它确实解释了再现困难的原因。 - David L
这也取决于它是在Debug还是Release中编译的:在Debug中有line 31,但在Release中throwthrow ex之间没有区别。我怀疑在Release中编译器会大量重写此代码。可能值得检查生成的实际IL以了解原因。 - Ed'ka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接