“x is null”和“x == null”的区别是什么?

574
在C# 7中,我们可以使用
if (x is null) return;

代替

if (x == null) return;

使用新方式(以前的示例)是否有任何优势?

语义是否有所不同?

如果不是仅仅因为口味,那么我应该在什么时候使用一种而不是另一种方式?

参考:C# 7.0有什么新功能.

3个回答

446

更新:Roslyn编译器已经更新,使得这两个运算符的行为在没有重载的等式运算符时相同。请查看当前编译器结果中的代码(代码中的M1M2),展示了当没有重载的等式比较器时会发生什么。它们现在都具有更好的性能表现的==行为。如果存在重载的等式比较器,则代码仍然不同

请查看旧版本的Roslyn编译器以下分析。
对于 null,它与我们在 C# 6 中习惯的没有区别。然而,当你将 null 更改为另一个常量时,事情变得有趣起来。
以这个为例:
Test(1);

public void Test(object o)
{
    if (o is 1) Console.WriteLine("a");
    else Console.WriteLine("b");
}

这个测试得到的是a。如果你把它与通常会写的o == (object)1进行比较,它确实有很大的不同。is考虑了比较另一侧的类型。这很酷!

我认为== nullis null的常量模式只是一些偶然非常熟悉的东西,其中is运算符和等号运算符的语法产生了相同的结果。


正如svick所评论的那样,is null调用System.Object::Equals(object, object),而==调用ceq

is的IL代码:

IL_0000: ldarg.1              // Load argument 1 onto the stack
IL_0001: ldnull               // Push a null reference on the stack
IL_0002: call bool [mscorlib]System.Object::Equals(object, object) // Call method indicated on the stack with arguments
IL_0007: ret                  // Return from method, possibly with a value

IL对于==

IL_0000: ldarg.1              // Load argument 1 onto the stack
IL_0001: ldnull               // Push a null reference on the stack
IL_0002: ceq                  // Push 1 (of type int32) if value1 equals value2, else push 0
IL_0004: ret                  // Return from method, possibly with a value

既然我们正在谈论 null,那么这没有区别,因为这 只对实例有影响。当您重载相等运算符时,情况可能会改变。


24
“看起来 is 运算符会调用 object.Equals(x, null) 方法,而 == 运算符编译成了 ceq 操作码。” 但你之前说过结果应该是相同的。 - svick
56
请谨记 == 运算符是可以被重载的,你可以让它表现出任何你想要的行为。例如,这个奇怪实现的 == 不能告诉你你的实例是否真正为空。另一方面,is null 永远返回 true 表示空引用 :) 如果你的代码中有 ReferenceEquals,VS 2017 会建议你将其更改为 is null,而不是 == null(这是正确的)。 - nawfal
6
@PatrickHofman是否应该反过来写为IL? == 调用System.Object::Equals(object, object)的是null,调用ceq。 - Zbigniew Ledwoń
1
因为将值类型装箱而使示例代码变得复杂而被踩。一旦你装箱了一个值,引用比较就很少有用了。 - Frank Hileman
4
从 C# 9.0 开始,引入了 "is not" 关键字,您可以使用 if (obj is not null) 替代 if (!(obj is null)),这将忽略 != 的使用。 - Majid Shahabfar
显示剩余4条评论

147

重载的等号运算符

实际上,当你比较null与一个已经重载了==运算符的类型时,这两种比较之间存在语义上的差异。foo is null将使用直接引用比较来确定结果,而foo == null当然会运行已经存在的重载==运算符。

在这个例子中,我引入了一个在重载的==运算符中的“bug”,导致它在第二个参数为null时总是抛出异常:

void Main()
{
    Foo foo = null;
    
    if (foo is null) Console.WriteLine("foo is null"); // This condition is met
    if (foo == null) Console.WriteLine("foo == null"); // This will throw an exception
}

public class Foo
{
    public static bool operator ==(Foo foo1, Foo foo2)
    {
        if (object.Equals(foo2, null)) throw new Exception("oops");
        return object.Equals(foo1, foo2);
    }
    
    // ...
}

对于foo is null的IL代码,使用ceq指令进行直接引用比较:

IL_0003:  ldloc.0     // foo
IL_0004:  ldnull      
IL_0005:  ceq

IL代码中的foo == null使用了对重载运算符的调用:
IL_0016:  ldloc.0     // foo
IL_0017:  ldnull      
IL_0018:  call        UserQuery+Foo.op_Equality

所以区别在于,如果你使用==,你就有可能运行用户代码(这可能会导致意外行为或性能问题)。
你还可以通过使用SharpLab在线工具,查看C#编译器如何转换Main方法。
private void Main()
{
    Foo foo = null;
    if ((object)foo == null)
    {
        Console.WriteLine("foo is null");
    }
    if (foo == null)
    {
        Console.WriteLine("foo == null");
    }
}

5
另外,需要注意的是,如果x是泛型类型,那么(x is null)需要一个类约束,而(x == null)和object.ReferenceEquals(x, null)则不需要。 - David Augusto Villa
9
还应该注意,空值合并运算符(??)和空值合并赋值运算符(??=)与“is”一样,也会忽略重载的等于运算符(==)。 - Majid Shahabfar
这是非常有用的答案,解释了为什么我的单元测试一直失败。 - Taher
最后一点似乎不再有效... 如果在约束上明确设置一个值类型(例如 bool IsNull<T>(T item) where T : struct => item is null;),它会失败,但是 bool IsNull<T>(T item) => item is null; 目前看起来是可以的 - Jcl

42

当你尝试将非空变量与空值进行比较时,也存在区别。使用==时,编译器会发出警告,而使用is时,编译器会发出错误。很可能,在99%的情况下,您希望编译器因这种基本错误而指责您。对于is null,加1。

输入图像描述

输入图像描述

P.S. 使用NetCore3.1在https://dotnetfiddle.net/上进行了测试。


1
这只是与你的 Visual Studio 设置有关。你可以轻易地告诉编译器将那些警告转换为编译错误。 - Benjamin Sutas
11
@BenjaminSutas 的确可以配置 VS 更改默认行为。但是默认行为通常是大多数用户所期望的行为。 - Frederic

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