在.NET异常中保留原始的StackTrace/LineNumbers

16
了解throw exthrow之间的区别,为什么在这个例子中保留了原始的StackTrace:
    static void Main(string[] args)
    {
        try
        {
            LongFaultyMethod();
        }
        catch (System.Exception ex)
        {
            Console.WriteLine(ex.StackTrace);
        }
    }

    static void LongFaultyMethod()
    {
        try
        {
            int x = 20;
            SomethingThatThrowsException(x);
        }
        catch (Exception)
        {
            throw;
        }
    }

    static void SomethingThatThrowsException(int x)
    {
        int y = x / (x - x);
    }

但在这一个中不行:

    static void Main(string[] args)
    {
        try
        {
            LongFaultyMethod();
        }
        catch (System.Exception ex)
        {
            Console.WriteLine(ex.StackTrace);
        }
    }

    static void LongFaultyMethod()
    {
        try
        {
            int x = 20;
            int y = x / (x - 20);
        }
        catch (Exception)
        {
            throw;
        }
    }
第二种情况会产生与 "throw ex" 相同的输出吗?
在两种情况下,人们都期望看到初始化y的行号。
2个回答

18

我不确定这个限制是在C#语言、CLI还是Microsoft对它们的实现中,但你的第二个示例是需要显式调用Exception.InternalPreserveStackTrace的情况,就像以下帖子所述。由于这个方法是internal的,通常必须通过反射调用。可以通过创建一个Action<Exception>来进行调用,从而几乎完全消除与此相关的性能问题,如本答案末尾所示。

参考: 重新抛出异常并保留完整的调用堆栈跟踪

编辑: 在重新审视ECMA-335 Partition I §12.4.2 (异常处理)和Partition III §4.24 (重新抛出异常)之后,我现在认为您看到的行为是CLR(Microsoft's implementation of the CLI)的语义错误。关于这种行为唯一的具体参考是“rethrow不会改变对象中的堆栈跟踪。”在这里描述的情况下,重新抛出实际上正在更改堆栈跟踪,使得PreserveStackTrace hack成为已知的CLR缺陷的解决方法。

static void LongFaultyMethod() 
{ 
    try 
    { 
        int x = 20; 
        int y = x / (x - 20); 
    } 
    catch (Exception ex) 
    { 
        PreserveStackTrace(ex); // <-- add this line
        throw; 
    } 
} 

PreserveStackTrace这里是那篇博客的优化版:

private static readonly Action<Exception> _internalPreserveStackTrace =
    (Action<Exception>)Delegate.CreateDelegate(
        typeof(Action<Exception>),
        typeof(Exception).GetMethod(
            "InternalPreserveStackTrace",
            BindingFlags.Instance | BindingFlags.NonPublic));

public static void PreserveStackTrace(Exception e)
{
    _internalPreserveStackTrace(e);
}

2
因为在第二个例子中,你是从同一个方法中重新抛出异常。而在第一个例子中,它是从不同的方法中抛出的。在一个方法的作用域中,堆栈跟踪只能有一个。
按照以下方式操作,最好的方法是始终将异常包装在新的异常中,以便您可以查看异常深度。
“如果在同一方法中发出了重新抛出命令(异常堆栈跟踪仅具有每个方法的一个行号信息,您永远无法看到堆栈跟踪,在方法A中,第2行抛出了异常,然后在同一方法A中,它从第17行重新抛出,它只包含从哪里重新抛出异常的最后一行号码。”
try        
{            
   int x = 20;            
   int y = x / (x - 20);        
}        
catch (Exception ex)        
{            
   // do something here.. like log or something
   throw new Exception("Internal Exception", ex);        
}

我很惊讶有这么多评论没有读我的评论!! 我在评论中写道,您应该安全地记录此日志,原因有很多,如果顶层代码吃掉了异常并且您不知道异常在哪里抛出,记录可以帮助您交叉异常!!!

如果您不需要记录日志,则不要捕获异常。


@Akash Kava:你有参考资料吗?我不确定你是否完全理解了OP的问题。 - Sam Harwell
重新抛出异常将不会保留行号,重新抛出异常将显示第一个堆栈项的行作为“rethrow”的行,如果您的异常在同一函数中被捕获并重新抛出,您将永远不知道异常是在哪里抛出的。让我们以XAML使用它,ASP.NET使用它为例,我认为微软的人们不会在不担心性能的情况下使用内部异常。 - Akash Kava
3
@280Z: 不需要。如果你没有有用的信息可以添加,那就根本不要捕获异常。 - John Saunders
@John Saunders:有许多原因需要捕获无法恢复并且不需要包装的异常,包括某些类型的日志记录和异常过滤(通过catch(Exception ex)处理一组异常类型,然后测试ex是否是一些特定类型中的一个,并对这些类型进行常规处理)。 - Sam Harwell
@280Z:在我的经验中,更高级别的处理程序无论如何都不能理解特定的异常;至少不足以真正“恢复”它。它怎么知道我的“FileNotFoundException”实际上意味着什么? - John Saunders
显示剩余6条评论

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