我该如何确定一个方法可能抛出哪些异常?

40
我的问题与这篇文章“在C#中确定方法可能抛出的异常”非常相似。然而,我真的想知道是否有一种方法可以确定给定方法可能抛出的所有异常的堆栈。我希望有一个工具或实用程序可以在编译时或通过反射分析代码,就像FxCop、StyleCop或NCover一样。我不需要在运行时获取此信息,我只想确保我们在代码中正确捕获和记录异常。
我们目前正在捕获我们知道的异常并记录所有通配符。这确实很有效; 但是,我只是希望有人使用过或知道一种可以发现此信息的工具。

是的,不幸的是我也得到了同样的错误。事实证明所需的逻辑比我想象的要复杂一些。然而,对于堆栈的一些虚拟推送/弹出处理应该可以解决问题-这正是我现在正在尝试的。 - Noldorin
澄清一下,您是指函数抛出的所有异常,就像开发人员编写的那样(例如函数中的throw命令),还是也包括其他被该函数调用的例程可能抛出的异常(例如.NET库调用引发的无效操作异常)? - rjzii
我只是想知道为什么你要重新发明轮子?如果已经有一个能够完美地做到这一点的工具,为什么还要编写代码来实现同样的功能呢? - Mark
9个回答

52

针对之前的回答,我已经成功创建了一个基本的异常查找器。它利用了一个基于反射的ILReader类,可在Haibo Luo的MSDN博客这里找到。(只需添加对项目的引用即可)

更新:

  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<T>对象来抛出。此外,它还维护本地变量数组和堆栈,以便跟踪不会立即在创建后抛出的异常。

当然,目前这段代码并不是万无一失的。我需要进行一些改进才能使其更加健壮,具体包括:

  1. 检测未直接使用异常构造函数抛出的异常。(例如,从本地变量或方法调用中检索异常。)
  2. 支持从堆栈中弹出而后又重新推入的异常。
  3. 添加流控制检测。处理任何抛出的异常的try-catch块应该从列表中删除相应的异常,除非检测到rethrow指令。

除此之外,我认为这段代码已经足够完整了。在我找到如何进行流控制检测之前,可能需要进行更多的调查(尽管我现在相信我已经看到了它在IL级别上是如何运作的)。

如果有人想要创建一个全功能的“异常分析器”,这些函数可能可以转化为整个库,但希望即使在当前状态下也提供了一个良好的起点。

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


1
如果可以的话,我会给它加上+1或+100分——这是一段非常棒的代码。我刚刚将它移植到了Mono.Cecil,谢谢。 - satnhak
2
这是一项非常出色的工作。同时,我想要一个独立的工具,可以指向一个程序集并找出特定方法会抛出哪些异常。如果对你们中的任何人有用,我已在Github上分享了源代码... https://github.com/stevesheldon/ExceptionReflector。它基于上述代码构建。 - Steve Sheldon
2
@Noldorin - 谢谢,很高兴能够帮忙!关于这个工具的更多信息,我在这里写了一篇简短的博客...http://steves-rv-travels.com/archives/167。 - Steve Sheldon
我现在有点沮丧。C# 在很多方面都比 Java 好,但是为了知道在 try/catch 块中需要考虑哪些异常,你必须使用反射来查找异常。在 Java 中,这只需简单地转到方法的定义,因为你抛出的任何异常都必须声明。我真的没有时间去获取别人的代码并运行它,只是为了找出抛出了什么异常,所以我想我只能放弃我的 try/catch 块了。 - froggythefrog
1
仅供参考,因为Steve原来的链接已经失效了:http://web.archive.org/web/20131209043117/http://steves-rv-travels.com/archives/167 - Marcus Mangelsdorf
显示剩余6条评论

13

这并不是非常困难。您可以像这样获取由方法创建的异常列表:

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共享库的Lokad.Quality.dll(它使用Mono.Cecil进行代码反射)。我实际上是将此代码放入主干中的一个测试用例中

比如,我们有一个像这样的类:

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之上实现任何方法查找),但应该是可行的。

这大致是完成任务的正确方式,但远未完整或健壮(您可能已经意识到了,但在您的答案中没有指出)。请注意,您仅检查在方法中直接创建的异常对象。您忽略了它们是否被抛出,是否通过字段或方法访问任何异常以及涉及堆栈的推送/弹出。 - Noldorin
感谢您的努力,有时候只能选择一个答案真的很难。 - Jamey McElveen

9

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


是的,如果你有足够的资金,这可能是最可靠/最简单的解决方案。然而,使用不过分冗长的代码也可以实现更简单且相当有效的方法,就像我所展示的那样。 - Noldorin
我能想到唯一需要看到每一个可能的异常的情况,就是在你有购买这种软件的钱的环境下。如果你正在编写免费代码,使用偶尔的 catch all 块然后重新引发错误可能是更好的解决方案。如果你正在编写需要卓越稳定性的关键代码,你将销售此代码,那么应考虑 Exception Hunter 等工具的成本以提高生产力。 - Mark

2

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

希望这能帮到你!


这确实有帮助,这就是我现在正在做的。然而,通常是客户发现了我没有捕获的错误,这就是为什么我想要事先知道所有的错误。谢谢! - Jamey McElveen
我想最好的选择是通过 MSDN 调查您正在使用的每种方法。 - James
是的,那就是我预期要做的。我只是想先在这里确认一下。谢谢! - Jamey McElveen

2

我非常怀疑在C#中有任何(至少是直接的)方法可以做到这一点。不过,我有一个可能会起作用的想法,所以请继续阅读...

首先,值得注意的是,使用大量参数排列进行暴力搜索显然是不可行的。即使具有参数类型的先前知识(我认为在您的情况下并不理想),任务在一般情况下本质上缩小为停机问题,因为您不知道函数是否会在给定某些参数时终止。理想情况下,异常应该会停止这个过程,但这当然并不总是发生。

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

  1. 对给定方法进行深度优先或广度优先搜索。找到在方法体中任何地方调用的所有方法/属性,并对它们进行递归处理。
  2. 对于方法/属性树中的每个 CIL 代码块,检查代码是否可能引发任何异常,并将其添加到列表中。
这甚至可以让您获取有关异常的详细信息,例如它们是否直接由被调用的方法抛出,还是在调用堆栈中更深处抛出,甚至包括异常消息本身。无论如何,我会考虑在今天下午尝试实现这个想法,然后告诉您这个想法的可行性如何。

此外,我可以假定您想要的不仅仅是阅读方法的XML文档吗? - Noldorin

1

1

与Java不同,C#没有检查异常的概念。

在宏观层面上,您应该捕获所有错误并记录或通知用户。当您了解方法可能引发的特定异常时,一定要适当地处理它们,但一定要让任何其他异常冒泡(最好)或记录它们,否则您将无法找到错误并且通常会使雇用人员的生活变得痛苦,以帮助减少错误列表 - 我曾经在那里,不好玩! :)


我完全同意,这就是为什么我想要在事先能够找出可能的异常列表。我想测试和模拟所有可能性。 - Jamey McElveen
如果您能编写覆盖每种可能情况的测试,那么您就可以实现这一点 :) - Derek Ekins

1
John Robbins曾经写过一系列关于创建FxCop规则的文章,其中包括一篇MSDN文章,可以指示哪些异常被抛出。这是为了警告缺少异常的XML文档,但思路是相同的。

0
这不是答案,而是在@Noldorin的伟大工作基础上进行的补充。我使用了上面的代码,并想到一个非常有用的工具,开发人员可以将其指向任意程序集/dll并查看抛出的异常列表。
通过在上述工作的基础上构建,我构建了一个完全符合要求的工具。我在GitHub上分享了源代码,供有兴趣的人使用。它非常简单,我只有几个小时的空闲时间来编写它,但如果您认为需要更新,请随意fork... Exception Reflector on Github

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