空值合并赋值运算符的行为

6

我想知道 ??= 运算符在后台是如何运行的。我有两个问题。

考虑以下示例:

string name = "John";
name ??= "George";

1) 这是否等同于 name = name ?? "George";

2) 它是否像这样工作,

if (name == null) {
   name = "George";
}

或者

if (name == null) {
   name = "George";
}
else {
   name = name;
}

https://stackoverflow.com/a/59300172/2946329 - Salah Akbari
1
如果(name!= null)- 这里是不是应该用“==”?而且这两种情况基本上是相同的,因为“else”分支没有任何有意义的操作(并且很可能会被编译器抛弃)。 - Alex Skalozub
1
这与第2个版本相同。如果您自己编写第二个版本,您会发现编译器会优化掉else分支中的代码。 - Emanuel Vintilă
1
Furkan,if (name != null) { 应该改为 if (name == null) { - TheMisir
您需要参考语言设计提案,其中包括降级形式: https://github.com/dotnet/csharplang/blob/master/proposals/csharp-8.0/null-coalescing-assignment.md - Julien Couvreur
3个回答

10

它将被评估为这个值:

string text = "John";
if (text == null)
{
    text = "George";
}
您可以使用 sharplab 查看实际发生的情况: https://sharplab.io/#v2:CYLg1APgAgTAjAWAFBQMwAJboMLoN7LpGYZQAs6AsgBQCU+hxTUcADOgHYCGAtgKboAvOgBEAKQD2ACw4iA3Mm790AfhXCRAcT4SATgHM+8xkQC+yU0A 进一步了解信息:https://stackoverflow.com/a/59300172/2946329 根据文档
引入了C#8.0空合并并赋值运算符??=。您可以使用??=运算符仅在左操作数评估为空时将右操作数的值分配给其左操作数。

我的第一个问题呢? - Furkan Öztürk
7
@FurkanÖztürk:请不要在一个问题中问两个问题。正如您刚刚发现的那样,通常只会回答其中一个问题。如果您有两个问题,请发布两个不同的问题。 - Eric Lippert

3
根据文档,null合并赋值运算符??=只有在左操作数评估为null时,才将其右操作数的值分配给左操作数。如果左操作数评估为非空,则??=运算符不会评估其右操作数。在您的代码示例中,它不会被评估,因为name不是null
string name = "John";
name ??= "George";

如果你写类似于这样的代码,它就能够正常工作

string name = null;
name ??= "George";

名称值将为George。 扩展变体是

if (name is null) //or name == null
{
    name = "George";
}

空值合并运算符??会返回左操作数的值,如果它不是null;否则,它会计算右操作数并返回结果。
在这个示例中name = name ?? "George"namenull时,结果将只是George。在你的样例中,name = name ?? "George";name ??= "George";的返回结果相同。但无论哪种情况下,只有在分配之前原始的namenull时,你才能获得George值。您还可以参考语言规范了解详细信息。

我的第一个问题呢? - Furkan Öztürk
@FurkanÖztürk 我正在更新一个答案。 - Pavel Anikhouski

1
你可以使用https://sharplab.io/来测试差异。 ??=??之间的区别非常小,实际上,在代码JIT编译后就消失了。
简而言之:
  1. 一旦代码编译为汇编语言,它们是相同的。
  2. 其等效于:
if (text == null){
    text = "George";
}

SharpLab 示例

此示例的代码:

public void M1() {
    string name = "John";
     name ??= "George";
    Console.WriteLine(name);
}

public void M2() {
    string name = "John";
    name = name  ?? "George";
    Console.WriteLine(name);
}

生成此中间 C# 代码,显示真正的差异:
public void M1()
{
    string text = "John";
    if (text == null)
    {
        text = "George";
    }
    Console.WriteLine(text);
}

public void M2()
{
    string text = "John";
    text = (text ?? "George");
    Console.WriteLine(text);
}

除了复制(dup)和弹出(pop)操作外,IL基本上与几乎相同。你可能会认为??在这方面有些更慢:

    IL_0000: nop
    IL_0001: ldstr "John"
    IL_0006: stloc.0
    IL_0007: ldloc.0
    IL_0008: brtrue.s IL_0010
    IL_000a: ldstr "George"
    IL_000f: stloc.0
    IL_0010: ldloc.0
    IL_0011: call void [System.Console]System.Console::WriteLine(string)
    IL_0016: nop
    IL_0017: ret

vs

        IL_0000: nop
        IL_0001: ldstr "John"
        IL_0006: stloc.0
        IL_0007: ldloc.0
***     IL_0008: dup
        IL_0009: brtrue.s IL_0011
***     IL_000b: pop
        IL_000c: ldstr "George"
        IL_0011: stloc.0
        IL_0012: ldloc.0
        IL_0013: call void [System.Console]System.Console::WriteLine(string)
        IL_0018: nop
        IL_0019: ret

但是发布模式下的程序集是相同的:

C.M1()
    L0000: mov ecx, [0x1a58b46c]
    L0006: test ecx, ecx
    L0008: jnz L0010
    L000a: mov ecx, [0x1a58b470]
    L0010: call System.Console.WriteLine(System.String)
    L0015: ret

C.M2()
    L0000: mov ecx, [0x1a58b46c]
    L0006: test ecx, ecx
    L0008: jnz L0010
    L000a: mov ecx, [0x1a58b470]
    L0010: call System.Console.WriteLine(System.String)
    L0015: ret

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接