【问题标题】:How can I determine which exceptions can be thrown by a given method?如何确定给定方法可以抛出哪些异常?
【发布时间】:2010-11-02 10:32:06
【问题描述】:

我的问题和"Finding out what exceptions a method might throw in C#"这个问题真的一样。但是,我真的很想知道是否有人知道一种方法来确定给定方法可能抛出的所有异常的堆栈。我希望有一个工具或实用程序可以在编译时或通过 FxCop、StyleCop 或 NCover 等反射分析代码。我在运行时不需要这些信息,我只想确保我们正在捕获异常并将它们正确记录到输出代码中。

我们目前正在捕获已知的异常并记录所有通配符。这确实很好用;但是,我只是希望有人使用或知道可以发现此信息的工具。

【问题讨论】:

  • 是的,不幸的是我遇到了同样的错误。事实证明,所需的逻辑再次比我想象的要复杂一些。然而,堆栈的一些虚拟推/拉处理应该可以完成这项工作 - 这正是我现在正在尝试的。
  • 澄清点,但您是指由开发人员编写的函数抛出的所有异常(例如函数中的抛出命令),或者也可能由调用的其他例程抛出函数(例如 .NET 库调用引发的无效操作异常)?
  • 我只是想知道你为什么要重新发明轮子?如果已经创建了一个工具可以做到这一点并且非常好,为什么还要编写有希望做到这一点的代码呢?

标签: c# exception error-handling


【解决方案1】:

这与其说是建立在上述@Noldorin 所做的伟大工作之上的答案。我使用了上面的代码,并认为拥有一个开发人员可以指向任意程序集/dll 并查看抛出的异常列表的工具会非常有用。

通过在上述工作的基础上进行构建,我构建了一个工具,可以做到这一点。我在 GitHub 上为任何感兴趣的人分享了源代码。超级简单,我只有几个小时的空闲时间来编写它,但如果您认为合适,请随时 fork 并进行更新...

Exception Reflector on Github

【讨论】:

    【解决方案2】:

    我为 Reflector 编写了一个名为 ExceptionFinder 的插件来处理这个问题。你可以在:

    http://exfinderreflector.codeplex.com/

    问候, 杰森

    【讨论】:

      【解决方案3】:

      按照我之前的回答,我设法创建了一个基本的异常查找器。它使用基于反射的ILReader 类,可在罗海波的 MSDN 博客上获得here。 (只需添加对项目的引用即可。)

      更新:

      1. 现在处理局部变量和堆栈。
        • 正确检测从方法调用或字段返回并随后抛出的异常。
        • 现在可以完全且适当地处理堆栈推送/弹出。

      这是完整的代码。您只是想将GetAllExceptions(MethodBase) 方法用作扩展或静态方法。

      using System;
      using System.Collections.Generic;
      using System.Collections.ObjectModel;
      using System.Linq;
      using System.Reflection;
      using System.Reflection.Emit;
      using System.Text;
      using ClrTest.Reflection;
      
      public static class ExceptionAnalyser
      {
          public static ReadOnlyCollection<Type> GetAllExceptions(this MethodBase method)
          {
              var exceptionTypes = new HashSet<Type>();
              var visitedMethods = new HashSet<MethodBase>();
              var localVars = new Type[ushort.MaxValue];
              var stack = new Stack<Type>();
              GetAllExceptions(method, exceptionTypes, visitedMethods, localVars, stack, 0);
      
              return exceptionTypes.ToList().AsReadOnly();
          }
      
          public static void GetAllExceptions(MethodBase method, HashSet<Type> exceptionTypes,
              HashSet<MethodBase> visitedMethods, Type[] localVars, Stack<Type> stack, int depth)
          {
              var ilReader = new ILReader(method);
              var allInstructions = ilReader.ToArray();
      
              ILInstruction instruction;
              for (int i = 0; i < allInstructions.Length; i++)
              {
                  instruction = allInstructions[i];
      
                  if (instruction is InlineMethodInstruction)
                  {
                      var methodInstruction = (InlineMethodInstruction)instruction;
      
                      if (!visitedMethods.Contains(methodInstruction.Method))
                      {
                          visitedMethods.Add(methodInstruction.Method);
                          GetAllExceptions(methodInstruction.Method, exceptionTypes, visitedMethods,
                              localVars, stack, depth + 1);
                      }
      
                      var curMethod = methodInstruction.Method;
                      if (curMethod is ConstructorInfo)
                          stack.Push(((ConstructorInfo)curMethod).DeclaringType);
                      else if (method is MethodInfo)
                          stack.Push(((MethodInfo)curMethod).ReturnParameter.ParameterType);
                  }
                  else if (instruction is InlineFieldInstruction)
                  {
                      var fieldInstruction = (InlineFieldInstruction)instruction;
                      stack.Push(fieldInstruction.Field.FieldType);
                  }
                  else if (instruction is ShortInlineBrTargetInstruction)
                  {
                  }
                  else if (instruction is InlineBrTargetInstruction)
                  {
                  }
                  else
                  {
                      switch (instruction.OpCode.Value)
                      {
                          // ld*
                          case 0x06:
                              stack.Push(localVars[0]);
                              break;
                          case 0x07:
                              stack.Push(localVars[1]);
                              break;
                          case 0x08:
                              stack.Push(localVars[2]);
                              break;
                          case 0x09:
                              stack.Push(localVars[3]);
                              break;
                          case 0x11:
                              {
                                  var index = (ushort)allInstructions[i + 1].OpCode.Value;
                                  stack.Push(localVars[index]);
                                  break;
                              }
                          // st*
                          case 0x0A:
                              localVars[0] = stack.Pop();
                              break;
                          case 0x0B:
                              localVars[1] = stack.Pop();
                              break;
                          case 0x0C:
                              localVars[2] = stack.Pop();
                              break;
                          case 0x0D:
                              localVars[3] = stack.Pop();
                              break;
                          case 0x13:
                              {
                                  var index = (ushort)allInstructions[i + 1].OpCode.Value;
                                  localVars[index] = stack.Pop();
                                  break;
                              }
                          // throw
                          case 0x7A:
                              if (stack.Peek() == null)
                                  break;
                              if (!typeof(Exception).IsAssignableFrom(stack.Peek()))
                              {
                                  //var ops = allInstructions.Select(f => f.OpCode).ToArray();
                                  //break;
                              }
                              exceptionTypes.Add(stack.Pop());
                              break;
                          default:
                              switch (instruction.OpCode.StackBehaviourPop)
                              {
                                  case StackBehaviour.Pop0:
                                      break;
                                  case StackBehaviour.Pop1:
                                  case StackBehaviour.Popi:
                                  case StackBehaviour.Popref:
                                  case StackBehaviour.Varpop:
                                      stack.Pop();
                                      break;
                                  case StackBehaviour.Pop1_pop1:
                                  case StackBehaviour.Popi_pop1:
                                  case StackBehaviour.Popi_popi:
                                  case StackBehaviour.Popi_popi8:
                                  case StackBehaviour.Popi_popr4:
                                  case StackBehaviour.Popi_popr8:
                                  case StackBehaviour.Popref_pop1:
                                  case StackBehaviour.Popref_popi:
                                      stack.Pop();
                                      stack.Pop();
                                      break;
                                  case StackBehaviour.Popref_popi_pop1:
                                  case StackBehaviour.Popref_popi_popi:
                                  case StackBehaviour.Popref_popi_popi8:
                                  case StackBehaviour.Popref_popi_popr4:
                                  case StackBehaviour.Popref_popi_popr8:
                                  case StackBehaviour.Popref_popi_popref:
                                      stack.Pop();
                                      stack.Pop();
                                      stack.Pop();
                                      break;
                              }
      
                              switch (instruction.OpCode.StackBehaviourPush)
                              {
                                  case StackBehaviour.Push0:
                                      break;
                                  case StackBehaviour.Push1:
                                  case StackBehaviour.Pushi:
                                  case StackBehaviour.Pushi8:
                                  case StackBehaviour.Pushr4:
                                  case StackBehaviour.Pushr8:
                                  case StackBehaviour.Pushref:
                                  case StackBehaviour.Varpush:
                                      stack.Push(null);
                                      break;
                                  case StackBehaviour.Push1_push1:
                                      stack.Push(null);
                                      stack.Push(null);
                                      break;
                              }
      
                              break;
                      }
                  }
              }
          }
      }
      

      总而言之,该算法通过读取 CIL 指令(以及跟踪已访问的方法)递归地枚举(深度优先)在指定方法中调用的任何方法。它维护一个可以使用HashSet&lt;T&gt; 对象抛出的集合列表,该对象在最后返回。它还维护了一个局部变量数组和一个堆栈,以跟踪在创建后没有立即抛出的异常。

      当然,此代码在当前状态下并非绝对可靠。我需要对其进行一些改进才能使其健壮,即:

      1. 检测未使用异常构造函数直接引发的异常。 (即异常是从局部变量或方法调用中检索到的。)
      2. 支持异常从堆栈中弹出,然后再推回。
      3. 添加流量控制检测。处理任何抛出异常的 Try-catch 块应从列表中删除相应的异常,除非检测到 rethrow 指令。

      除此之外,我相信代码是合理完整的。在我弄清楚如何进行流控制检测之前可能需要进行更多调查(尽管我相信我现在可以看到它在 IL 级别是如何运行的)。

      如果要创建一个功能齐全的“异常分析器”,这些函数可能会变成一个完整的库,但希望这至少可以为这样的工具提供一个良好的起点,如果它的功能还不够好的话当前状态。

      无论如何,希望对您有所帮助!

      【讨论】:

      • 我在上面解释过的这段代码有一些问题。我无法在评论中正确格式化:-|。
      • +1,如果可以的话,+100 - 这绝对是一段​​很棒的代码。我刚刚将它移植到 Mono.Cecil,谢谢。
      • 这是一项非常棒的工作。同时我想要一个独立的工具,我可以指向一个程序集并找出特定方法会抛出哪些异常。如果这对你们中的任何人有用,我在 github 上分享了源代码...github.com/stevesheldon/ExceptionReflector。它建立在上面的代码之上。
      • @Noldorin - 谢谢,很高兴!有关该工具的更多信息,我在这里写了一篇关于它的快速博客...steves-rv-travels.com/archives/167
      • 仅供参考,因为 Steve 的原始链接已失效:web.archive.org/web/20131209043117/http://steves-rv-travels.com/…
      【解决方案4】:

      这应该不会太难。您可以获取由这样的方法创建的异常列表:

      IEnumerable<TypeReference> GetCreatedExceptions(MethodDefinition method)
      {
          return method.GetInstructions()
              .Where(i => i.OpCode == OpCodes.Newobj)
              .Select(i => ((MemberReference) i.Operand).DeclaringType)
              .Where(tr => tr.Name.EndsWith("Exception"))
              .Distinct();
      }
      

      片段使用来自开源 Lokad Shared Libraries 的 Lokad.Quality.dll(它使用 Mono.Cecil 来完成代码反射的繁重工作)。我其实是put this code into one of the test cases in trunk

      说,我们有一个这样的类:

      class ExceptionClass
      {
          public void Run()
          {
              InnerCall();
              throw new NotSupportedException();
          }
      
          void InnerCall()
          {
              throw new NotImplementedException();
          }
      }
      

      然后为了从 Run 方法中获取所有异常:

      var codebase = new Codebase("Lokad.Quality.Test.dll");
      var type = codebase.Find<ExceptionClass>();
      var method = type.GetMethods().First(md => md.Name == "Run");
      
      var exceptions = GetCreatedExceptions(method)
          .ToArray();
      
      Assert.AreEqual(1, exceptions.Length);
      Assert.AreEqual("NotSupportedException", exceptions[0].Name);
      

      现在剩下的就是将方法调用堆栈向下遍历到某个深度。您可以像这样获取方法引用的方法列表:

      var references = method.GetReferencedMethods();
      

      现在,在能够对堆栈中的任何方法调用 GetCreatedExceptions 之前,我们只需要实际查找代码库并将所有 MethodReference 实例解析为实际包含字节码的 MethodDefinition 实例(带有一些缓存以避免扫描现有的分支)。这是代码中最耗时的部分(因为 Codebase 对象没有在 Cecil 之上实现任何方法查找),但这应该是可行的。

      【讨论】:

      • 这模糊地是完成任务的正确方法,但它远非完整或强大(您可能已经意识到,但没有在您的答案中指出)。请注意,您只检查直接在方法中创建的异常对象。您忽略它们是否被抛出,是否通过字段或方法访问任何异常,以及涉及堆栈的推送/弹出。
      • 感谢您的努力,有时只能选择一个答案。
      【解决方案5】:

      此答案已发布在您引用的另一个问题中,我知道我之前曾在另一个类似问题中推荐过它。你应该试试Exception Hunter。它列出了每个可能引发的异常。当我第一次在我的代码上运行它时,我对这个列表的大小感到非常惊讶,即使是简单的函数也是如此。有 30 天的免费试用期,所以没有理由不试一试。

      【讨论】:

      • 是的,这可能是最可靠/最简单的解决方案,因为您有钱可以拿出来。然而,正如我所展示的,使用不荒谬的代码长度可以实现更简单且相当有效的方法。
      • 只有在你有钱购买这种软件的环境中,我才能看到你需要在哪里看到所有可能的异常。如果您正在编写免费代码,则偶尔使用 catch all 块然后重新抛出错误可能是更好的解决方案。如果您正在编写需要异常稳定性的关键代码,您将出售此代码,然后应考虑 Exception Hunter 等工具的成本以提高您的工作效率。
      【解决方案6】:

      我非常怀疑在 C# 中是否有任何(至少是直接的)方法可以做到这一点。话虽如此,我确实有一个可能可行的想法,所以请继续阅读......

      首先,值得注意的是,使用大量参数排列进行暴力搜索显然是不可行的。即使具有参数类型的先验知识(我认为在您的情况下不希望这样做),在一般情况下,该任务基本上会减少到halting problem,因为您不知道该函数将终止给定的某些参数。理想情况下,异常应该会阻止这种情况,但当然并非总是如此。

      现在,也许最可靠的方法是分析源代码(或更实际的 CIL 代码)本身,看看可能会抛出哪些异常。我相信这实际上可能是可行的。一个简单的算法可能类似于:

      1. 对给定方法进行深度优先或广度优先搜索。查找在方法主体中的任何位置调用的所有方法/属性,并对它们进行递归。
      2. 对于方法/属性树中的每个 CIL 代码块,检查代码中可能引发的任何异常,然后添加到列表中。

      这甚至可以让您获得有关异常的详细信息,例如它们是由被调用的方法直接抛出,还是在调用堆栈的更深处,甚至是异常消息本身。不管怎样,我会考虑在今天下午晚些时候尝试实现这个,所以我会告诉你这个想法有多可行。

      【讨论】:

      • 另外,我可以假设您想要的不仅仅是阅读方法的 XML 文档吗?
      【解决方案7】:

      John Robbins 有一系列关于创建 FxCop 规则的文章,其中包括一个 MSDN article,它会指示引发了哪些异常。这是为了警告缺少异常的 XML 文档,但想法是一样的。

      【讨论】:

        【解决方案8】:

        不像java C#没有检查异常的概念。

        在宏观层面上,您应该捕获所有内容并记录或通知用户错误。当您了解方法可能引发的特定异常时,然后明确地适当处理该异常,但请确保让任何其他异常冒泡(最好)或记录它们,否则您将遇到无法找到的错误,并且通常无法生存对于任何被雇用来帮助减少错误列表的人来说都是悲惨的——去过那里,不好玩! :)

        【讨论】:

        • 我同意 100% 这就是为什么我希望能够事先找出可能的例外情况列表。我想测试和模拟所有的可能性。
        • 如果您设法编写涵盖所有可能场景的测试,您将实现这一目标:)
        【解决方案9】:

        对于这种情况,我的方法是处理我想要处理的所有异常,然后覆盖应用程序的 UnhandledException 事件以记录我不知道的任何其他异常。然后,如果我遇到任何我认为可以解决的问题,我就会相应地更新。

        希望有帮助!

        【讨论】:

        • 这确实有帮助,这就是我现在正在做的事情。然而,通常是客户发现了我没有捕获的客户,这就是为什么我想提前了解他们。谢谢!
        • 我想最好的选择是通过 MSDN 调查您使用的每种方法。
        • 是的,这就是我所期望的。我只是想先检查这里。谢谢!
        猜你喜欢
        • 2011-11-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-22
        • 1970-01-01
        • 2014-01-15
        • 1970-01-01
        相关资源
        最近更新 更多