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

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

 public class GitLog { FileSystemHelper fileSystem; ///  /// Initializes a new instance of the  class. ///  /// The path to a Git working copy. /// A helper class that provides file system services (optional). /// Thrown if the path is invalid. /// Thrown if there is no Git repository at the specified path. 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,如此屏幕fileSystem所示(踩到下一行会抛出NullReferenceException ): 什么时候为null不为空?

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

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

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

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

如果在TestFoo方法的末尾设置断点,您可能会看到_foo变量集,但它仍将在调试器中显示为null。

但是,如果你随后对_foo任何事情 ,那么它就会正确显示。 即使是简单的任务,如

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

如果你单步执行它,你会看到_foo显示为null,直到它被赋值给f

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

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

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

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

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

我不是汇编/ 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 

将其与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 

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

其他人在这个问题上遇到了同样的问题。 有趣的是它也使用了this._field = expression ?? new ClassName(); this._field = expression ?? new ClassName(); 格式。 它可能是调试器的某种问题,因为写出值似乎可以为它们产生正确的结果。

尝试添加调试/日志代码以在分配后显示字段的值,以消除附加调试器中的怪异。