【问题标题】:How to hide the current method from exception stack trace in .NET?如何从 .NET 中的异常堆栈跟踪中隐藏当前方法?
【发布时间】:2011-02-27 17:40:57
【问题描述】:

我想知道是否有办法从方法内部抛出异常,但不将该方法包含在异常堆栈跟踪中。例如

void ThrowSomeException()
{
    throw new SomeException();
}

然后,如果我从名为Foo() 的方法调用该方法,我希望异常堆栈跟踪以at Foo() 开头,而不是at ThrowSomeException()。我假设如果这是可能的,它可能是通过在方法上使用属性。

我对一般答案感兴趣,但如果这不可能,我真正想做的是为IEnumerable 创建一个扩展方法AssertEqual(),我将在NUnit 测试中使用它。所以当我调用myEnumerable.AssertEqual(otherEnumerable) 失败时,NUnit 应该在测试方法内部报错,而不是在扩展方法内部报错。

谢谢!

【问题讨论】:

  • 你看过 CollectionAssert 助手了吗? NUnit 2.4+;听起来你在追求 AreEqual 或 AreEquivalent。
  • 啊,谢谢——我是 NUnit 的新手。不过,如果有人知道的话,我仍然对第一个问题的答案感兴趣。
  • +1 因为你问了一个疯狂的问题,但解释了原因。我无法告诉您看到一个疯狂的问题并想知道为什么有人会想要被问到的东西让我感到多么烦恼。干得好!

标签: c# .net exception


【解决方案1】:

现在从 .NET 6 开始使用 StackTraceHiddenAttribute 内置

https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.stacktracehiddenattribute?view=net-6.0

[StackTraceHidden]
void ThrowSomeException()
{
    throw new SomeException();
}

【讨论】:

    【解决方案2】:

    也许在不久的将来,您可以使用 [StackTraceHidden]。目前,System.Diagnostics.StackTraceHiddenAttribute 是内部的,但 Consider exposing System.Diagnostics.StackTraceHiddenAttribute publicly 正在运行。

    【讨论】:

      【解决方案3】:

      我根据StriplingWarrior的解决方案创建了一个扩展方法,效果很好。

      public static class ExceptionExtensions
      {
          [DebuggerHidden]
          [MethodImpl(MethodImplOptions.AggressiveInlining)]
          public static void Throw(this Exception exception) => throw exception;
      }
      

      然后我们就可以使用它了……

      using static SomeNamespace.ExceptionExtensions;
      
      public class SomeClass
      {
          private void SomeMethod(string value)
          {
              var exception = GetArgumentException(nameof(value), value);
              exception?.Throw(); // only throw if any exception was getted
      
              ... //method implementation
          }
      
          private Exception GetArgumentException(string paramName, string value)
          {
              if (value == null)
                  return new ArgumentNullException(paramName);
              if (string.IsNullOrEmpty(value))
                  return new ArgumentException("value is empty.", paramName);
      
              return null;
          }
      }
      

      这样Throw() 方法就不会出现在堆栈跟踪中。

      【讨论】:

        【解决方案4】:

        如果您告诉编译器积极内联您的方法,它应该首先阻止您的方法进入调用堆栈:

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        void ThrowSomeException()
        {
            throw new SomeException();
        }
        

        此属性自 .NET 4.5 起可用。

        但是,这仅被视为对编译器的强提示,在某些情况下它仍然不会导致内联。例如,如果您从不同的程序集调用方法,或者在调试模式下编译,我认为它不能内联它。

        一种解决方法可能是仅使用您的帮助程序创建异常,然后从调用代码中抛出它。

        public static InvalidOperationException InvalidOperation(FormattableString message)
        {
          return new InvalidOperationException(FormattableString.Invariant(message));
        }
        
        // calling code
        throw ExceptionHelpers.InvalidOperation($"{0} is not a valid value", value);
        

        但是,如果您的辅助方法具有确定是否抛出异常的逻辑,那可能对您不起作用:

        public static class Require
        {
            [ContractAnnotation("condition:false => halt")]
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            [DebuggerHidden]
            public static void True(bool condition)
            {
                if (!condition)
                {
                    throw ExceptionHelpers.InvalidOperation($"Expected condition was not met.");
                }
            }
        }
        

        在这些情况下,您可能不得不处理异常堆栈跟踪,正如此处的其他答案所示。例如,您可能希望忽略标有DebuggerHiddenAttribute 的方法。

        【讨论】:

          【解决方案5】:

          请注意,这是对现有答案的改进。


          这个问题的Accepted answer 真的很笨拙,因为
          1. 它使用纯字符串通过名称确定我们需要从堆栈跟踪中隐藏的方法。
          2. 拆分堆栈跟踪基于string.Split 方法。
          3. 它只隐藏了StackTrace 属性中的一种方法,没有更多。

          但它会覆盖 StackTrace 属性本身(这是问题的期望行为)


          Most Upvoted Answer 非常干净,因为
          1. 它使用了一个属性,而不是将方法的名称指定为一个字符串。
          2. 它可用于对StackTrace 隐藏多个方法。

          但它真的很复杂,只是增加了两个类 用于扩展方法。 而且其中最重要的弱点不是压倒一切 StackTrace 属性本身。


          在阅读了前两个解决方案之后,我想我达到了最简单和最干净的方式(结合了这个问题的两个最佳答案)

          这是所需的基础设施。

          [AttributeUsage(AttributeTargets.Method, Inherited = false)]
          public sealed class StackTraceHiddenAttribute : Attribute
          {
          }
          
          public class SomeException : Exception
          {
              public override string StackTrace
              {
                  get
                  {
                      return string.Concat(
                          new StackTrace(this, true)
                              .GetFrames()
                              .Where(frame => !frame.GetMethod().IsDefined(typeof(StackTraceHiddenAttribute), true))
                              .Select(frame => new StackTrace(frame).ToString())
                              .ToArray());
                  }
              }
          }
          

          这里是使用先前基础设施的示例

          [StackTraceHidden] // apply this to all methods you want to be omitted in stack traces
          static void Throw()
          {
              throw new SomeException();
          }
          
          static void Foo()
          {
              Throw();
          }
          
          static void Main()
          {
              try
              {
                  Foo();
              }
              catch (Exception e)
              {
                  Console.WriteLine(e.StackTrace);
              }                  
          }      
          

          编辑 根据@Stakx 对此答案的评论,在放置后立即删除,他指出了一些重要的想法:
          此解决方案仅适用于自定义定义的异常,他的解决方案适用于所有异常类型,这是绝对正确的。
          据此,这里有一种扩展方法,不需要复杂得多,它可以解决问题并适用于所有异常类型。

          public static class ExceptionExtensions
          {
              public static string GetStackTraceWithoutHiddenMethods(this Exception e)
              {
                  return string.Concat(
                     new StackTrace(e, true)
                         .GetFrames()
                         .Where(frame => !frame.GetMethod().IsDefined(typeof(StackTraceHiddenAttribute), true))
                         .Select(frame => new StackTrace(frame).ToString())
                         .ToArray());
              }                         
          }
          

          这几乎与他的代码相同,除了集成了IsDefined 方法。

          【讨论】:

          • 这个基本的想法对我很有效。我有一堆单元测试助手类,我想对单元测试运行器隐藏它们。我所做的唯一更改是我已经在所有这些上设置了 [DebuggerHidden],因此我只是过滤了该属性而不是自定义属性。
          【解决方案6】:

          GetStackTraceWithoutHiddenMethods() 扩展方法的答案很好,除了 Exception.ToString() 不使用 StackTrace 属性,而是调用 GetStackTrace(),这不是可覆盖的。因此,如果希望将此扩展方法与他们自己的基于 Exception 的类型一起使用,他们必须重写 ToString() 而不是重写 StackTrace 属性。

          【讨论】:

            【解决方案7】:

            我猜您想这样做是为了整合用于创建异常的代码?
            既然如此,不如写一个ThrowException()函数,为什么不写一个GetException()函数呢?然后在 Foo 中,只需执行 throw GetException();

            【讨论】:

            • 在这种特定情况下并不完全正确,但在其他情况下是个好主意,谢谢
            • 在想要 if (Disposed) throw new ObjectDisposedException() 之类的东西时不起作用。
            【解决方案8】:

            使用此答案末尾的代码可以编写如下代码:

            [HideFromStackTrace] // apply this to all methods you want omitted in stack traces
            static void ThrowIfNull(object arg, string paramName)
            {
                if (arg == null) throw new ArgumentNullException(paramName);
            }
            
            static void Foo(object something)
            {
                ThrowIfNull(something, nameof(something));
                …
            }
            
            static void Main()
            {
                try
                {
                    Foo(null);
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.GetStackTraceWithoutHiddenMethods());
                }                  // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            }                      // gets a stack trace string representation
                                   // that excludes all marked methods
            

            这是一种可能的实现方式:

            using System;
            using System.Diagnostics;
            using System.Linq;
            using System.Reflection;
            
            [AttributeUsage(AttributeTargets.Method, Inherited=false)]
            public class HideFromStackTraceAttribute : Attribute { }
            
            public static class MethodBaseExtensions
            {
                public static bool ShouldHideFromStackTrace(this MethodBase method)
                {
                    return method.IsDefined(typeof(HideFromStackTraceAttribute), true);
                }
            }
            
            public static class ExceptionExtensions
            {
                public static string GetStackTraceWithoutHiddenMethods(this Exception e)
                {
                    return string.Concat(
                        new StackTrace(e, true)
                            .GetFrames()
                            .Where(frame => !frame.GetMethod().ShouldHideFromStackTrace())
                            .Select(frame => new StackTrace(frame).ToString())
                            .ToArray());  // ^^^^^^^^^^^^^^^     ^
                }                         // required because you want the usual stack trace
            }                             // formatting; StackFrame.ToString() formats differently
            

            请注意,这只会导致标记的方法从堆栈跟踪的一个特定表示中排除,而不是从堆栈跟踪本身中排除。我不知道如何实现后者。

            PS:如果您只想在调试会话期间在调用堆栈窗口中隐藏方法,只需将[DebuggerHidden] attribute 应用于该方法即可。

            【讨论】:

            • 迄今为止最好的解决方案。并且可以扩展,因为您可以向属性添加其他数据并启用扩展格式。您还可以在属性中定义一些预编译器条件以支持不同的调试模式或使用静态类进行。 (发布,Customer_Test,测试,Test_Full,...)。
            • 我投票支持 Paolo 的解决方案(因为它可以用于无法使用新属性修改代码的情况),但这仍然是一个很酷的解决方案!
            【解决方案9】:

            也许您可以派生自己的异常类型并覆盖 StackTrace 属性 getter 以排除您的方法:

            using System;
            using System.Collections.Generic;
            
            class MyException : Exception {
            
                string _excludeFromStackTrace;
            
                public MyException(string excludeFromStackTrace) {
                    _excludeFromStackTrace = excludeFromStackTrace;
                }
            
                public override string StackTrace {
                    get {
                        List<string> stackTrace = new List<string>();
                        stackTrace.AddRange(base.StackTrace.Split(new string[] {Environment.NewLine},StringSplitOptions.None));
                        stackTrace.RemoveAll(x => x.Contains(_excludeFromStackTrace));
                        return string.Join(Environment.NewLine, stackTrace.ToArray());
                    }
                }
            }
            
            class Program {
            
                static void TestExc() {
                    throw new MyException("Program.TestExc");
                }
            
                static void foo() {
                    TestExc();
                }
            
                static void Main(params string[] args) {
                    try{
                        foo();
                    } catch (Exception exc){
                        Console.WriteLine(exc.StackTrace);
                    }
                }
            
            }
            

            【讨论】:

            • 嗯似乎有点笨拙,但我现在的感觉是这可能是唯一的方法,所以我将其标记为答案,谢谢。
            • 堆栈跟踪字符串/数组摆弄的另一种方法:改用System.Diagnostics.StackTrace 类:new StackTrace(someException, skipFrames: …).ToString()。 (但是,请注意 StackTrace.ToString() 默认情况下不会发出文件名和行号,因此必须从 StackFrame 对象中派生这些,而这些对象可以从 StackTrace 派生而来。)
            猜你喜欢
            • 1970-01-01
            • 2017-06-29
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-12-15
            • 2011-01-05
            • 1970-01-01
            相关资源
            最近更新 更多