问题 为什么在Exception.StackTrace中会截断堆栈?


为什么堆栈的高位(在Exception.StackTrace中)被截断? 让我们看一个简单的例子:

public void ExternalMethod()
{
  InternalMethod();
}

public void InternalMethod()
{
  try
  {
    throw new Exception();
  }
  catch(Exception ex)
  {
    // ex.StackTrace here doesn't contain ExternalMethod()!
  }
}

看起来这是“按设计”。但这种奇怪设计的原因是什么?它只会使调试变得更复杂,因为在日志消息中我无法理解谁调用了InternalMethod(),而且这些信息通常非常必要。

至于解决方案(对于那些不知道的人),我理解有两种通用解决方案:
1)我们可以记录静态Environment.StackTrace属性,该属性包含整个堆栈(例如,从hiest级别(消息队列)开始并以发生异常的最深层方法结束)。
2)我们必须捕获并记录最高级别的异常。当我们需要捕获较低级别的异常来做某事时,我们需要重新抛出(在C#中使用“throw”语句)它会进一步提升。

但问题是关于这种设计的原因。


9575
2017-11-11 22:17


起源

为什么一个物体应该关心谁叫它?您的观点(2),即应该重新抛出异常,是正确的方法。 - Jeremy McGee
出于调试目的,主要包含堆栈跟踪信息以及异常。对于现有的设计,如果它具有较高的堆栈部分,则无法进行调试。因为,我再说一遍,知道谁调用该方法对调试非常有用。 - nightcoder


答案:


好的,现在我看到你得到了什么...抱歉我对内联的东西感到困惑。

捕获异常中的“堆栈”仅是从当前正在执行的catch块到抛出异常的位置的增量。从概念上讲,这种行为是正确的,因为Exception.StackTrack会告诉您在此try / catch块的上下文中发生异常的位置。这允许异常堆栈在“虚拟”调用之间转发,并仍然保持准确性。这种方法的一个典型例子是.Net Remoting异常。

因此,如果您想在catch块中获得完整的堆栈报告,则可以将当前堆栈添加到异常堆栈中,如下例所示。唯一的问题是这可能更昂贵。

    private void InternalMethod()
    {
        try
        {
            ThrowSomething();
        }
        catch (Exception ex)
        {
            StackTrace currentStack = new StackTrace(1, true);
            StackTrace exceptionStack = new StackTrace(ex, true);
            string fullStackMessage = exceptionStack.ToString() + currentStack.ToString();
        }
    }

11
2017-11-12 01:39



+1这是原因,请参阅我的答案以获得解决方法 - Sam Saffron


正如csharptest所说这是设计的。 StackTrace在try块停止。更进一步说,在抛出异常时调用框架中没有钩子。

因此,您可以做的最好的事情就是这些,它是获得完整堆栈跟踪的绝对要求(在创建异常时存储完整的跟踪):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.CompilerServices;
using System.Diagnostics;

namespace ConsoleApplication15 {

    [global::System.Serializable]
    public class SuperException : Exception {

        private void SaveStack() {
            fullTrace = Environment.StackTrace;
        }

        public SuperException() { SaveStack(); }
        public SuperException(string message) : base(message) { SaveStack();  }
        public SuperException(string message, Exception inner) : base(message, inner) { SaveStack(); }
        protected SuperException(
          System.Runtime.Serialization.SerializationInfo info,
          System.Runtime.Serialization.StreamingContext context)
            : base(info, context) { }

        private string fullTrace; 
        public override string StackTrace {
            get {
                return fullTrace;
            }
        }
    }

    class Program {

        public void ExternalMethod() {
            InternalMethod();
        }

        public void InternalMethod() {
            try {
                ThrowIt();
            } catch (Exception ex) {
                Console.WriteLine(ex.StackTrace);
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public void ThrowIt() {
            throw new SuperException();
        }


        static void Main(string[] args) {
            new Program().ExternalMethod();
            Console.ReadKey();
        }
    }
}

输出:

 
     在System.Environment.get_StackTrace()
   在C:\ Users \ sam \ Desktop \ Source中的ConsoleApplication15.SuperException..ctor()
\ ConsoleApplication15 \ ConsoleApplication15 \ Program.cs:第17行
   在C:\ Users \ sam \ Desktop \ Source \ Cons中的ConsoleApplication15.Program.ThrowIt()
oleApplication15 \ ConsoleApplication15 \ Program.cs:第49行
   在C:\ Users \ sam \ Desktop \ Sour中的ConsoleApplication15.Program.InternalMethod()中
ce \ ConsoleApplication15 \ ConsoleApplication15 \ Program.cs:第41行
   在C:\ Users \ sam \ Desktop \ S中的ConsoleApplication15.Program.Main(String [] args)
ource \ ConsoleApplication15 \ ConsoleApplication15 \ Program.cs:第55行
   在System.AppDomain._nExecuteAssembly(程序集程序集,String [] args)
   在Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   在System.Threading.ExecutionContext.Run(ExecutionContext executionContext,C
ontextCallback回调,对象状态)
   在System.Threading.ThreadHelper.ThreadStart()

不可能将此行为注入到现有的系统定义的异常中,但.Net具有丰富的基础结构,用于包装异常和重新抛出,因此它不应该是一个大问题。


2
2017-11-12 02:07



这是另一种方法;但是,它不会指出抛出异常的位置,而是它的构造位置(通常是相同的,所以这也不错)。如果你走这条路线,意识到它在调用上下文中是不准确的(与远程处理一样),并且还需要额外的代码来序列化'fullTrace'属性。 - csharptest.net
发表评论。 - Sam Saffron


如果你这样做,我知道在一个陷阱中 throw ex; 它会截断该点的堆栈跟踪。只是因为投掷它可能是“按设计” throw; 不会截断catch中的堆栈。同样可能会在这里发生,因为你抛出了一个新的异常。

如果您造成实际例外会发生什么(即 int i = 100/0;)?堆栈跟踪是否仍然被截断?


0
2017-11-11 22:28



throw; 也截断堆栈 - Enerccio


这通常是由编译器优化引起的。

您可以使用以下属性装饰您不想内联的方法:

[MethodImpl(MethodImplOptions.NoInlining)]
public void ExternalMethod()
{
  InternalMethod();
}

-1
2017-11-11 22:33



不,这不是因为内联。我所说的行为是默认行为。你可以自己看看。它总是像我所展示的那样。 - nightcoder
为什么JIT不会总是以可预测的方式内联方法?也许你的方法总是被内联。 - Niall Connaughton
它在你的函数中总是这样的事实并不能证明它不是由于内联。 - Ed S.
对不起-1,但这不是原因。 - Sam Saffron
同意山姆,thx消除混乱;) - csharptest.net