【问题标题】:Can Visual Studio tell me which reference threw a NullReferenceException?Visual Studio 可以告诉我哪个引用引发了 NullReferenceException?
【发布时间】:2012-01-14 12:28:52
【问题描述】:

我正在为 MVC Web 应用程序编写单元测试,但我收到了空引用异常,因为模拟的测试对象仅部分初始化。我知道哪一行抛出了异常,它看起来像这样:

return Supervisor.RegistrationInformation.Registrations
    .Any(r =>
        r.RegistrationCountry.IsUSAOrCandada() &&
        (!DatesWorked.Start.HasValue || r.RegistrationDate <= DatesWorked.Start.Value) &&
        (!DatesWorked.End.HasValue || r.RegistrationExpirationDate >= DatesWorked.End.Value) &&
        //...

那里有很多参考资料,其中任何一个都可能是问题所在。然而,NullReferenceException 本身似乎并没有捕捉到哪个引用被炸毁了。我传入 lambda 的事实带来了另一个挑战:据我所知,我无法在调试期间单步执行 lambda 并查看 r 的哪些成员为空。

我有什么办法可以做到以下一项或两项:

  • 让 Visual Studio 准确告诉我是哪个引用抛出了NullReferenceException
  • 如果做不到这一点,有没有办法让调试器在 Any 评估它时单步执行 lambda 表达式(或将鼠标悬停在事物上以查看它们的值)?

我觉得一定有办法做这些事情,但我似乎找不到。我使用的是 VS2010 Premium,并且安装了 Resharper、VS Power Tools 和其他几个扩展。如果有一个插件可以做到这一点,我会很好的。

编辑:

正如 Eric Lippert 所指出的,在发布配置中编译代码时,不可能查明 NR 异常的来源。我只是询问在调试模式下工作。如果 Visual Studio(或 VS 的某些扩展)可以在调试时跟踪引用的来源,那将回答我的问题。

编辑 2:

第二个问题——如何打破和单步执行 lambda——已经得到解答,但我仍然想知道是否有自动方法来追踪空引用。

【问题讨论】:

  • 不,没有办法做到这一点,除非 ReSharper 可以提供帮助。
  • 你有没有试过把 lambda 改成匿名函数并在里面放断点?
  • 启用第一次机会异常处理(Debug => Exceptions)并在抛出 NullReferenceException 时中断?
  • 将代码分解成更小的部分,每个部分只有一个引用。
  • 可以在 lambda 中设置断点——只是不能以通常的方式(单击装订线将断点包含语句,而不是 lambda 的内部)。您必须将光标放在 lambda 内并按 F9 - 然后您将在 lambda 内获得一个断点。

标签: c# visual-studio debugging resharper nullreferenceexception


【解决方案1】:

一般来说,没有一种方法可以做你想做的事,不。要了解原因,请考虑引发空引用异常时发生的情况。假设你是编译器,你必须发出代码来处理对 abc.Def.Ghi.Jkl() 的调用,其中 abc 是本地的,Def 和 Ghi 是引用类型的字段,而 Jkl 是方法。没有 IL 指令可以做这么复杂的事情。你必须把它分解。所以你为一个等价的程序发出代码,其中一切都简单得多。你发出程序片段:

temp1 = abc.Def;
temp2 = temp1.Ghi;
temp2.Jkl();

假设 temp2 为空,因为 Ghi 为空。在调用 Jkl 之前不会发现这个事实,此时 抛出异常的东西不知道 temp2 是如何初始化的。 这发生在很久以前,过去的一纳秒和机器代码没有过去的记忆;空引用并没有在其上保留一点说明 null 来自何处的注释,就像您说“a = b + c”时一样,生成的数字 12 并没有保留一个说明“我是b 和 c 之和”。

【讨论】:

  • 感谢您的洞察力,Eric,很高兴您能回答这些问题。我处于调试模式的事实是否给了我任何选择?我相当确定优化的构建会完全按照您的描述进行,但我认为可能有一个选项可以让调试器跟踪这样的事情(毫无疑问,以性能为代价)。
  • @JustinMorgan:不客气。回答您的后续问题:这些天的调试器有很多我不知道的功能!奇怪的是,我不是调试器的高级用户。考虑到这个东西现在拥有的功能数量,很可能有一种模式可以让你及时回顾并问“这个值是如何到达这里的?”如果有,我不知道是什么。
  • 虽然运行代码不会也不应该记下,但抖动肯定会在执行地址和 IL 指令之间创建一个映射(假设优化足够低),它应该能够从中推断出哪个变量或表达式导致NullReferenceException。它已经为其他目的保留了类似的映射。不知道它是否确实创建了这个特定的映射,但原则上它应该能够做到。
  • @CodeInChaos 让 CLR 保留一个指令指针->IL 偏移映射(您可以通过运行带有 CLR Profiler 的调试器并使用 COR_PRF_ENABLE_JIT_MAPS 标志来实现)仍然不够,因为为了准确推断出在特定 IL 偏移中引用了哪个 C# 表达式,您必须反编译该方法,因为 pdb 文件提供的 ILOffset->Code Range 映射不是细粒度的足够了(它们通常映射到语句,而不是表达式)。
  • @EricLippert Visual Studio 调试器中没有这样的功能,但它确实作为我共同创作的 Visual Studio 商业扩展的一部分存在,名为Statement Visualization
【解决方案2】:

这不会解决您的全部问题,但应该会有所帮助:

您可以在 lambda 中设置断点 - 只是不是以通常的方式(单击装订线将断点包含语句,而不是 lambda 的内部)。您必须将光标放在 lambda 内并按 F9 - 然后您将在 lambda 内获得一个断点。

【讨论】:

    【解决方案3】:

    针对您的特定问题的一种解决方案是在多行中重写 lambda,逐个评估每个条件并显式返回。然后,您可以更轻松地跟踪它并找到 null 参考。

    【讨论】:

    • 这个问题的前两段是废话,但最后一段是中肯的建议。 Any 不偷懒。
    • 哇。我不知道我在想什么。 Any() 在 foreach 中?噗噗。我纠正了它。谢谢。
    • 我认为这是一种选择,如果我最终没有找到问题代码,我会选择使用它。我想知道是否有可能在不这样做的情况下进入 lambda(这确实是可能的;请参阅问题下的 Joe White 评论)。
    【解决方案4】:

    如果您在此处设置断点,您应该能够在执行该行并引发异常之前检查每个值。您只需有条不紊地查看每个取消引用的项目,直到找到 Null 一个。 Visual Studio 非常擅长这类事情。

    【讨论】:

    • 我的问题是能够破坏 lambda 表达式,或者理想情况下让调试器告诉我罪魁祸首,这样我就可以节省一些时间。 Joe White 对这个问题的评论告诉我如何打破 lambda。
    【解决方案5】:

    可以在 lambda 表达式中放置一个断点,当它命中时,您应该能够将鼠标悬停在表达式上并查看它们的值。

    查看您的代码,我可以看到三个表达式中只有一个可能导致 NullRef - rr.RegistrationCountryDatesWorked

    将这三个表达式放在 Watch 窗口中,并要求调试器在任何 NullReferenceException 上中断(通过 Debug->Exceptions),或者在 lambda 表达式中放置一个断点,并使其成为条件 @987654324 上的条件断点@,答案应该很快就会出现。

    【讨论】:

    • 在我发布问题时,我不知道如何在 lambda 表达式中获取断点,尽管该特定部分已得到解答。不过,条件断点和 break-on-NRE 的想法都是很好的建议。 +1。
    【解决方案6】:

    我通常不会做出不回答的答案,但我认为说没有一般方法可以做到这一点的答案是不正确的。

    在我看来,您可以编写一个包装函数,获取一个表达式树,分解具有属性和字段访问器的每个子表达式,并使用显式空检查重构它,从而在每个访问器上抛出一个信息异常。伪代码:

    static Expression<Func<T, bool>> WithNullDebugging(Expression<Func<T, bool>> exp)
    {
        for each node in the expression tree
            if node is a field, property, or method accessor
                generate a null check for this member and an exception throw
                substitute the checked node for this node
            else if the node has subexpression children
                call this method recursively on each child
                substitute each checked subexpression for the subexpression
    
        return the fixed expression tree
    }
    

    我通常不使用 C# 进行元编程,所以我不确定,但我认为这很有可能;如果不是,请聪明的人告诉我,我将删除或修复此答案。

    【讨论】:

    • 我喜欢这个主意。这就像 lambda 表达式的调试扩展。这个特殊问题已经解决了,但是如果我将来可以使用这样的东西,我会记住它。谢谢。
    【解决方案7】:

    直接的问题是 lambda 将许多复杂的逻辑包装在一个语句中,因此您无法找到崩溃发生的位置。

    但这只是副作用。 真正的问题是您的代码错误地假设所有引用都不会为空。

    一种方法是尝试隔离崩溃并在“破损的位”上缠上绷带。但这不会攻击问题的根源:代码中有未经检查的假设,并且您已经有证据证明其中至少有一个是错误的。如果另一个错误,那么在未来某个未定义的时间点,您的程序可能会再次崩溃,您将再次调试和包扎。这种情况会一直持续下去,而且您的代码每次都会被黑客入侵。

    您需要放下调试器并考虑代码。所有代码,一次通过。 “桌面检查”它:遍历表达式的每个部分并问自己“这个位可以为空吗?如果是会发生什么?如果是,我怎样才能使它安全?”

    这样,您将能够以您知道可以识别空值的形式重写整个表达式,并且您永远不需要调试它来找出它爆炸的原因。

    例如,这个:

        r.RegistrationCountry.IsUSAOrCandada() && 
    

    ...如果 r==null 或 r.RegistrationCountry==null,则可能导致 null 取消引用。代码需要检查这些可能性。 “最具防御性”的代码是检查每个引用,如下所示:

        r != null && r.RegistrationCountry != null && r.RegistrationCountry.IsUSAOrCandada() && 
    

    保证每一步只有在上一步成功的情况下才会执行。但请注意,该列表可能永远不会提供 r==null,因此可能不需要检查。或者 r.RegistrationCountry 可能是一个结构(一个不可为空的类型),所以你会知道检查是不需要的。因此,您可以通过考虑避免不必要的检查。但是您需要仔细考虑代码的每个部分,以挑战和消除所有假设。

    【讨论】:

      【解决方案8】:

      是的。您需要在 Visual Studio 中安装 Resharper 扩展。 Resharper 是持续代码质量分析工具。

      您可以在下面的链接中找到更多详细信息

      https://www.jetbrains.com/resharper/

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-01-09
        相关资源
        最近更新 更多