【问题标题】:CLR System.NullReferenceException when forcing 'Set Next Statement' into 'if' block强制“设置下一条语句”进入“if”块时出现 CLR System.NullReferenceException
【发布时间】:2015-03-06 10:10:14
【问题描述】:

背景

我接受这不是在正常代码执行期间可能发生的事情,但我在调试时发现了它,并认为分享它很有趣。

我认为这是由 JIT 编译器引起的,但欢迎任何进一步的想法。

我已经使用 VS2013 针对 4.5 和 4.5.1 框架复制了这个问题:


设置

要查看此异常Common Language Runtime Exceptions,必须启用: DEBUG > Exceptions...

我已将问题的原因提炼为以下示例:

using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication6
{
    public class Program
    {
        static void Main()
        {
            var myEnum = MyEnum.Good;

            var list = new List<MyData>
            {
                new MyData{ Id = 1, Code = "1"},
                new MyData{ Id = 2, Code = "2"},
                new MyData{ Id = 3, Code = "3"}
            };

            // Evaluates to false
            if (myEnum == MyEnum.Bad) // BREAK POINT 
            {
                /*
                 * A first chance exception of type 'System.NullReferenceException' occurred in ConsoleApplication6.exe

                   Additional information: Object reference not set to an instance of an object.
                 */
                var x = new MyClass();

                MyData result;
                //// With this line the 'System.NullReferenceException' gets thrown in the line above:
                result = list.FirstOrDefault(r => r.Code == x.Code);

                //// But with this line, with 'x' not referenced, the code above runs ok:
                //result = list.FirstOrDefault(r => r.Code == "x.Code");
            }
        }
    }

    public enum MyEnum
    {
        Good,
        Bad
    }

    public class MyClass
    {
        public string Code { get; set; }
    }

    public class MyData
    {
        public int Id { get; set; }
        public string Code { get; set; }
    }
}

复制

if (myEnum == MyEnum.Bad) 上放置一个断点并运行代码。 当断点被命中时,Set Next Statement(Ctrl+Shift+F10) 成为if 语句的左大括号和运行到:

接下来,注释 out 第一个 lamda 语句并注释 in 第二个 - 因此不使用 MyClass 实例。 重新运行进程(打断,强制进入if 语句并运行)。你会看到代码正常工作:

最后,注释 in 第一个 lamda 语句并注释 out 第二个 - 所以使用了 MyClass 实例 。然后将if语句的内容重构为新方法:

using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication6
{
    public class Program
    {
        static void Main()
        {
            var myEnum = MyEnum.Good;

            var list = new List<MyData>
            {
                new MyData{ Id = 1, Code = "1"},
                new MyData{ Id = 2, Code = "2"},
                new MyData{ Id = 3, Code = "3"}
            };

            // Evaluates to false
            if (myEnum == MyEnum.Bad) // BREAK POINT 
            {
                MyMethod(list);
            }
        }

        private static void MyMethod(List<MyData> list)
        {
            // When the code is in this method, it works fine
            var x = new MyClass();

            MyData result;

            result = list.FirstOrDefault(r => r.Code == x.Code);
        }
    }

    public enum MyEnum
    {
        Good,
        Bad
    }

    public class MyClass
    {
        public string Code { get; set; }
    }

    public class MyData
    {
        public int Id { get; set; }
        public string Code { get; set; }
    }
}

重新运行测试,一切正常:


结论?

我的假设是 JIT 编译器已将 lamda 优化为始终为空,并且在初始化实例之前运行了一些进一步优化的代码。

正如我之前提到的,这在生产代码中永远不会发生,但我很想知道发生了什么。

【问题讨论】:

    标签: c# .net c#-4.0 lambda clr


    【解决方案1】:

    这是一个不可避免的事故,与优化无关。通过使用 Set Next Statement 命令,您绕过了 更多 代码,而不是您可以从源代码中轻松看到的代码。只有当您查看生成的机器代码时,它才会变得明显。在断点处使用 Debug + Windows + Disassembly。你会看到:

                // Evaluates to false
                if (myEnum == MyEnum.Bad) // BREAK POINT 
    0000016c  cmp         dword ptr [ebp-3Ch],1 
    00000170  setne       al 
    00000173  movzx       eax,al 
    00000176  mov         dword ptr [ebp-5Ch],eax 
    00000179  cmp         dword ptr [ebp-5Ch],0 
    0000017d  jne         00000209 
    00000183  mov         ecx,2B02C6Ch               // <== You are bypassing this
    00000188  call        FFD6FAE0 
    0000018d  mov         dword ptr [ebp-7Ch],eax 
    00000190  mov         ecx,dword ptr [ebp-7Ch] 
    00000193  call        FFF0A190 
    00000198  mov         eax,dword ptr [ebp-7Ch] 
    0000019b  mov         dword ptr [ebp-48h],eax 
                {
    0000019e  nop 
                    /*
                     * A first chance exception of type 'System.NullReferenceException' occurred in ConsoleApplication6.exe
    
                       Additional information: Object reference not set to an instance of an object.
                     */
                    var x = new MyClass();
    0000019f  mov         ecx,2B02D04h             // And skipped to this
    000001a4  call        FFD6FAE0 
    // etc...
    

    那么,那个神秘的密码是什么?这不是您在程序中明确编写的任何内容。您可以使用“反汇编”窗口中的“设置下一条语句”命令来查找。将其移至地址00000183,即 if() 语句之后的第一个可执行代码。开始单步执行,你会看到它执行了一个名为ConsoleApplication1.Program.&lt;&gt;c__DisplayClass5的类的构造函数

    现有的 SO 问题已经很好地涵盖了其他方面,这是为您的源代码中的 lambda 表达式自动生成的类。需要在程序中存储捕获的变量list。由于您跳过了它的创建,因此在 lambda 中取消引用 list 总是会被 NRE 炸毁。

    “泄漏抽象”的标准案例,C# 有一些,但并非如此。当然,您对此无能为力,您当然可以责怪调试器没有正确猜测,但这是一个非常难以解决的问题。它不能轻易找出该代码是属于 if() 语句还是它后面的代码。一个设计问题,调试信息是基于行号的,并且没有代码行。通常,x64 抖动也是一个问题,即使在简单的情况下也会出现问题。应该在 VS2015 中修复。

    这是您必须通过 Hard Way™ 学习的东西。如果它真的非常重要,那么我向您展示了如何正确设置下一个语句,您必须在反汇编视图中执行它才能使其工作。请随时在 connect.microsoft.com 上报告此问题,但如果他们还不知道,我会感到惊讶。

    【讨论】:

    • 谢谢。我查看了 IL,但无法理解它。反汇编窗口不是我所知道的,所以这很有帮助。
    猜你喜欢
    • 2012-02-10
    • 2018-09-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-02
    • 2018-12-15
    相关资源
    最近更新 更多