【问题标题】:Why doesn't the null coalescing operator (??) work in this situation?为什么空合并运算符 (??) 在这种情况下不起作用?
【发布时间】:2013-10-21 12:57:51
【问题描述】:

当我运行此代码时,我得到了一个意外的 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.");
            }
        }

当我在调试器中单步执行代码时,在我跨过第一行(使用 ?? 运算符)之后,fileSystem 的值仍然为 null,如此屏幕截图所示(跨过下一行抛出NullReferenceException):

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

ReSharper 指出该字段仅用于这一种方法,因此可能会转换为局部变量...所以我尝试了,猜猜是什么?有效。所以,我有我的解决方法,但我无法终生明白为什么上面的代码不起作用。我觉得我正在学习一些关于 C# 的有趣的东西,或者我做了一些非常愚蠢的事情。谁能看到这里发生了什么?

【问题讨论】:

  • 您已经在方法参数中声明fileSystemHelpernull,我不确定,但这可能与它有关。但话又说回来,我猜。
  • 您确定 NRE 不会发生在 within GetFullPath(忽略手表显示的内容)吗?我看不出上面的代码会导致上述行为。
  • 好的,从 VisualStudio 退出去做其他事情然后重新加载它,现在一切正常,我无法重现问题。我认为这可能是 ReSharper 单元测试运行程序的一个奇怪的缓存问题。我正在使用一个简单的 MSpec 测试来练习代码。 ReSharper 在运行单元测试时会获取程序集的卷影副本,有时,只是有时,卷影副本似乎“卡住”了,我以前见过几次。所以很可能,我实际上是在运行旧代码,即使我已经手动重建了所有内容。这是我最好的解释......

标签: c# null-coalescing-operator


【解决方案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 技能的人可以对幕后发生的事情有所了解。

另外有趣的是,如果您将 null 合并运算符替换为等效的:

_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 

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

【讨论】:

  • 这似乎是 64 位调试器的问题。以“任何 CPU”为目标的构建和编译会表现出这种行为。改成target x86,那就没问题了。
  • 我想我知道问题出在哪里了...... VS 在 64 位构建中生成的行号不正确。为空合并行之后的行生成的行号实际上位于空合并指令的“中间”。因此调试器在该行中断,但前一条指令尚未完全完成。越过那条线将完成该指令。如果您逐步进行反汇编,您可以看到设置成员变量的点。
  • @JeffMercado - 已确认。它只对“任何 CPU”或“x64”执行此操作。适用于“x86”。
  • 我在 Connect 上打开了一个错误,使用上面的 repro 源代码并链接到这个问题。如果您可以重现该问题,那么您可以访问问题报告并单击“我也可以”,其中显示“X 用户可以重现此问题”和/或投票。它几乎不是一个表演停止者,但它确实会引起很多混乱和浪费时间。 connect.microsoft.com/VisualStudio/feedback/details/805334/…
  • “它在 RyuJIT 中工作”是否可以解决这个问题?
【解决方案2】:

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

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

【讨论】:

    猜你喜欢
    • 2017-10-19
    • 1970-01-01
    • 2023-03-17
    • 1970-01-01
    • 1970-01-01
    • 2017-11-26
    • 2010-12-29
    • 1970-01-01
    • 2021-08-05
    相关资源
    最近更新 更多