为什么空合并运算符(??)在这种情况下不起作用?

Tim*_*ong 15 c# null-coalescing-operator

NullReferenceException当我运行此代码时,我发现了一个意外,省略了fileSystemHelper参数(因此将其默认为null):

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.");
            }
        }
Run Code Online (Sandbox Code Playgroud)

当我单步执行调试器中的代码时,在我跳过第一行(使用??运算符)之后fileSystem仍然具有值null,如此屏幕剪辑所示(踩到下一行抛出NullReferenceException): 什么时候为null不为空?

这不是我的预期!我期望null合并运算符发现参数为null并创建一个new FileSystemHelper().我已经盯着这段代码多年了,看不出它有什么问题.

ReSharper指出该字段仅用于这一种方法,因此可能会被转换为局部变量......所以我试过了,猜猜是什么?有效.所以,我有我的修复,但我不能为我的生活看到为什么上面的代码不起作用.我觉得我正处于学习C#有趣的东西的边缘,无论是那个还是我做过一些非常愚蠢的事情.谁能看到这里发生了什么?

Mat*_*int 12

我在VS2012中使用以下代码复制了它:

public void Test()
{
    TestFoo();
}

private Foo _foo;

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

public class Foo
{
}
Run Code Online (Sandbox Code Playgroud)

如果在TestFoo方法结束时设置断点,则可能会看到_foo变量集,但在调试器中它仍将显示为null.

但是,如果你那么做什么_foo,它就会显示正确.即使是简单的任务,如

_foo = foo ?? new Foo();
var f = _foo;
Run Code Online (Sandbox Code Playgroud)

如果您单步执行它,您将看到_foo在分配给它之前显示为null f.

这让我想起了延迟执行行为,例如LINQ,但我找不到任何可以证实的行为.

完全有可能这只是调试器的一个怪癖.也许拥有MSIL技能的人可以了解幕后发生的事情.

同样有趣的是,如果用等效的替换null合并运算符:

_foo = foo != null ? foo : new Foo();
Run Code Online (Sandbox Code Playgroud)

然后它不会表现出这种行为.

我不是汇编/ MSIL人,但只是看看两个版本之间的dissasembly输出很有意思:

        _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 
Run Code Online (Sandbox Code Playgroud)

将其与内联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 
Run Code Online (Sandbox Code Playgroud)

基于此,我确实认为存在某种延迟执行.与第一个示例相比,第二个示例中的赋值语句非常小.

  • 这似乎是64位调试器的问题.构建和编译目标"任何CPU"都表现出这种行为.将其更改为目标x86,然后它不再是一个问题. (3认同)
  • 我想我知道问题是什么...... VS在64位版本中生成的行号是不正确的.为空合并行之后的行生成的行号实际上是在空合并指令的"中间".因此调试器在该行上断开,但前一条指令尚未完全完成.单步执行该行将完成该指令.如果单步执行反汇编,则可以看到成员变量的设置点. (3认同)