CLR System.NullReferenceException при форсировании "Установить следующий оператор" в блок "if"

Фон

Я согласен, что это не то, что может произойти во время обычного выполнения кода, но я обнаружил его во время отладки и думал, что интересно поделиться.

Я думаю, что это вызвано компилятором JIT, но приветствуем дальнейшие мысли.

Я реплицировал эту проблему, ориентированную на рамки 4.5 и 4.5.1, используя VS2013:

VS2013 Premium 12.0.31101.00 Update 4. NET 4.5.50938


Настройка

Чтобы увидеть это исключение, необходимо включить Common Language Runtime Exceptions: DEBUG > Exceptions...

Common Language Runtime Exceptions enabled

Я объяснил причину проблемы следующим примером:

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 и выполняется до:

NullReferenceException thrown

Затем запишите первый оператор lamda и комментарий во втором - поэтому экземпляр MyClass не используется. Перезапустите процесс (нажав на перерыв, заставив оператор if и запустив). Вы увидите, что код работает правильно:

MyClass instantiated correctly

Наконец, комментируйте первый оператор lamda и закомментируйте второй - так что используется MyClass instance . Затем преобразуйте содержимое инструкции 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; }
    }
}

Повторите тест, и все работает правильно:

MyClass instantiated correctly inside MyMethod


Заключение

Мое предположение заключается в том, что компилятор JIT оптимизировал lamda всегда равным нулю, а еще один оптимизированный код работает до инициализации экземпляра.

Как я уже говорил, это никогда не может произойти в производственном коде, но мне было бы интересно узнать, что происходит.

Ответ 1

Это довольно неизбежная неудача, не связанная с оптимизацией. Используя команду Set Next Statement, вы обходите больше кода, чем вы можете легко увидеть из исходного кода. Это становится очевидным только при взгляде на сгенерированный машинный код. Используйте Debug + Windows + Демонтаж в точке останова. Вы увидите:

            // 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...

Итак, что это за таинственный код? Это не то, что вы написали в своей программе явно. Вы можете узнать, используя команду Set Next Statement в окне "Разборка". Переместите его в адрес 00000183, первый исполняемый код после инструкции if(). Начните с шага, вы увидите, что он выполняет конструктор класса с именем ConsoleApplication1.Program.<>c__DisplayClass5

В противном случае, хорошо охваченный существующими вопросами SO, это автоматически сгенерированный класс для выражения лямбда в вашем исходном коде. Для хранения захваченных переменных list требуется ваша программа. Поскольку вы пропустили его создание, разыменование list в лямбде всегда будет бомбить с помощью NRE.

Стандартный случай "негерметичной абстракции", С# имеет некоторые из них, но не возмутительно. Конечно, вы ничего не можете с этим поделать, конечно, вы можете обвинить отладчика в том, что он не догадывается об этом правильно, но это очень трудная проблема. Невозможно легко узнать, принадлежит ли этот код оператору if() или следующему ему коду. Проблема дизайна, информация об отладке - это номер строки, на которой нет строки кода. Также, как правило, проблема с джиттером x64, она путается даже в простых случаях. Который должен быть исправлен в VS2015.

Это то, что вам нужно изучить Hard Way ™. Если это действительно так важно, я показал вам, как правильно установить следующий оператор, вы должны сделать это в представлении "Разборка", чтобы он работал. Не стесняйтесь сообщать об этой проблеме на сайте connect.microsoft.com, я был бы удивлен, если бы они еще не знали об этом.