Почему в этой ситуации не работает нулевой коалесцирующий оператор (?)?

Я получаю неожиданный NullReferenceException, когда я запускаю этот код, опуская параметр fileSystemHelper (и, следовательно, по умолчанию его не имеет значения):

public class GitLog
    {
    FileSystemHelper fileSystem;

    /// <summary>
    ///   Initializes a new instance of the <see cref="GitLog" /> class.
    /// </summary>
    /// <param name="pathToWorkingCopy">The path to a Git working copy.</param>
    /// <param name="fileSystemHelper">A helper class that provides file system services (optional).</param>
    /// <exception cref="ArgumentException">Thrown if the path is invalid.</exception>
    /// <exception cref="InvalidOperationException">Thrown if there is no Git repository at the specified path.</exception>
    public GitLog(string pathToWorkingCopy, FileSystemHelper fileSystemHelper = null)
        {
        this.fileSystem = fileSystemHelper ?? new FileSystemHelper();
        string fullPath = fileSystem.GetFullPath(pathToWorkingCopy); // ArgumentException if path invalid.
        if (!fileSystem.DirectoryExists(fullPath))
            throw new ArgumentException("The specified working copy directory does not exist.");
        GitWorkingCopyPath = pathToWorkingCopy;
        string git = fileSystem.PathCombine(fullPath, ".git");
        if (!fileSystem.DirectoryExists(git))
            {
            throw new InvalidOperationException(
                "There does not appear to be a Git repository at the specified location.");
            }
        }

Когда я нахожу один шаг кода в отладчике, после того, как я перехожу через первую строку (с оператором ??), тогда fileSystem все еще имеет значение null, как показано на этом экране snip (переход по следующей строке бросает NullReferenceException): When is null not null?

Это не то, что я ожидал! Я ожидаю, что оператор нулевого коалесцирования обнаружит, что параметр имеет значение null и создает new FileSystemHelper(). Я давно уставился на этот код и не вижу, что с ним не так.

ReSharper указал, что поле используется только в этом методе, поэтому его можно было бы преобразовать в локальную переменную... поэтому я попытался это и угадал, что? Это сработало. Итак, у меня есть исправление, но я не могу, чтобы жизнь меня поняла, почему код выше не работает. Я чувствую, что я нахожусь на грани изучения чего-то интересного о С#, либо это, либо я сделал что-то действительно немое. Может ли кто-нибудь увидеть, что здесь происходит?

Ответы

Ответ 1

Я воспроизвел его в VS2012 со следующим кодом:

public void Test()
{
    TestFoo();
}

private Foo _foo;

private void TestFoo(Foo foo = null)
{
    _foo = foo ?? new Foo();
}

public class Foo
{
}

Если вы установите точку останова в конце метода TestFoo, вы ожидаете увидеть набор переменных _foo, но он все равно будет отображаться как нулевой в отладчике.

Но если вы затем сделаете что-нибудь с _foo, оно появится правильно. Даже простое назначение, например

_foo = foo ?? new Foo();
var f = _foo;

Если вы пройдете через него, вы увидите, что _foo показывает значение null, пока не будет присвоено значение f.

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

Совершенно возможно, что это просто причуда отладчика. Возможно, кто-то с навыками MSIL может пролить свет на то, что происходит под капотом.

Также интересно, что если вы замените нулевой оператор коалесцирования эквивалентом:

_foo = foo != null ? foo : new Foo();

Тогда он не проявляет этого поведения.

Я не парень сборки /MSIL, но просто взглянуть на результат разборки между двумя версиями интересно:

        _foo = foo ?? new Foo();
0000002d  mov         rax,qword ptr [rsp+68h] 
00000032  mov         qword ptr [rsp+28h],rax 
00000037  mov         rax,qword ptr [rsp+60h] 
0000003c  mov         qword ptr [rsp+30h],rax 
00000041  cmp         qword ptr [rsp+68h],0 
00000047  jne         0000000000000078 
00000049  lea         rcx,[FFFE23B8h] 
00000050  call        000000005F2E8220 
        var f = _foo;
00000055  mov         qword ptr [rsp+38h],rax 
0000005a  mov         rax,qword ptr [rsp+38h] 
0000005f  mov         qword ptr [rsp+40h],rax 
00000064  mov         rcx,qword ptr [rsp+40h] 
00000069  call        FFFFFFFFFFFCA000 
0000006e  mov         r11,qword ptr [rsp+40h] 
00000073  mov         qword ptr [rsp+28h],r11 
00000078  mov         rcx,qword ptr [rsp+30h] 
0000007d  add         rcx,8 
00000081  mov         rdx,qword ptr [rsp+28h] 
00000086  call        000000005F2E72A0 
0000008b  mov         rax,qword ptr [rsp+60h] 
00000090  mov         rax,qword ptr [rax+8] 
00000094  mov         qword ptr [rsp+20h],rax 

Сравните это с версией inlined-if:

        _foo = foo != null ? foo : new Foo();
0000002d  mov         rax,qword ptr [rsp+50h] 
00000032  mov         qword ptr [rsp+28h],rax 
00000037  cmp         qword ptr [rsp+58h],0 
0000003d  jne         0000000000000066 
0000003f  lea         rcx,[FFFE23B8h] 
00000046  call        000000005F2E8220 
0000004b  mov         qword ptr [rsp+30h],rax 
00000050  mov         rax,qword ptr [rsp+30h] 
00000055  mov         qword ptr [rsp+38h],rax 
0000005a  mov         rcx,qword ptr [rsp+38h] 
0000005f  call        FFFFFFFFFFFCA000 
00000064  jmp         0000000000000070 
00000066  mov         rax,qword ptr [rsp+58h] 
0000006b  mov         qword ptr [rsp+38h],rax 
00000070  nop 
00000071  mov         rcx,qword ptr [rsp+28h] 
00000076  add         rcx,8 
0000007a  mov         rdx,qword ptr [rsp+38h] 
0000007f  call        000000005F2E72A0 
        var f = _foo;
00000084  mov         rax,qword ptr [rsp+50h] 
00000089  mov         rax,qword ptr [rax+8] 
0000008d  mov         qword ptr [rsp+20h],rax 

Исходя из этого, я думаю, что происходит какое-то отсроченное исполнение. Оператор присваивания во втором примере очень мал по сравнению с первым примером.

Ответ 2

Кто-то другой столкнулся с той же проблемой в этом вопросе. Интересно, что он также использует формат this._field = expression ?? new ClassName();. Это может быть какая-то проблема с отладчиком, поскольку запись значения вне показала для них правильные результаты.

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