“ReferenceEquals(myObject, null)”是否比“myObject == null”更好的实践方式?

59

我有个同事喜欢把他的空值检查写成下面这样:

if (!ReferenceEquals(myObject, null))

然而,就我个人而言,我认为这种语法阅读起来很繁琐,更喜欢使用以下语法:

if (myObject != null)

我找到一些文章和stackoverflow的问题,讨论了关于在运算符重载方面使用ReferenceEquals的好处,但是除了运算符重载的情况外,使用ReferenceEquals相对于使用==有哪些好处呢?


2
值得注意的是,你朋友的版本可读性较差(当检查点可互换时,这将成为一个问题,而它们通常是可互换的)。 - keyser
还有一个更糟糕的问题 - 你必须键入 if (!Object.ReferenceEquals(myObject, null)) - 所以实际上比描述的还要更长。 - Reed Copsey
1
@ReedCopsey 不需要输入那个,因为你的方法位于继承了静态“ReferenceEquals”方法的类或结构体中,其(直接或间接)基类是“System.Object”。因此,可以像上面那样编写(使用“new”方法隐藏继承的方法当然可能会发生,但那是一个邪恶的例外)。 - Jeppe Stig Nielsen
1
@ReedCopsey,你可以轻松地判断ReferenceEquals是否被隐藏(或者是否定义了另一个匹配的重载)。但是如果要遵循你的哲学,那么可以说global::System.Object.ReferenceEquals(myObject, null),假设有人定义了其他名为ObjectSystem的类型或命名空间... - Jeppe Stig Nielsen
1
@JeppeStigNielsen 或者直接使用“object.Ref..”,因为这是语言定义的。不过,我的个人哲学是使用 == :) - Reed Copsey
显示剩余2条评论
11个回答

55
在运算符重载场景之外,使用ReferenceEquals与==相比有什么好处吗? 没有 - 明确使用Object.ReferenceEquals的唯一优点(我认为这并不是很大的优点)是它永远不会使用重载的等于运算符。在非重载情况下,对于所有“引用类型(string除外)”,== Operator被定义为“如果其两个操作数引用同一个对象,则返回true”。因此,它的等效性相同(前提是它没有被重载)。
我个人也更喜欢使用第二种语法,并发现它更易于维护以进行空值检查。我还会争辩说,任何重载的operator==也应该对null进行适当的检查,在某些原因导致它没有检查null的情况下(这将是奇怪的),你可能希望使用重载而不是ReferenceEquals。

37

使用C# 7,您可以使用:

if ( !(myObject is null) )

它相当于

if (!ReferenceEquals(myObject, null))

2
你确定吗?C#参考说:“表达式的值由对静态Object.Equals(expr, constant)方法的调用确定。”而且if ( !(myObject is null) )SharpLab中转换为if (myObject != null)。相关:https://dev59.com/xFgQ5IYBdhLWcg3wazIi#42814441 - Kevinoid
1
@Kevinoid - 现在,该引用指的是“对于引用类型,它使用(object)x == null。”我认为这意味着它明确地调用了object实现,这确实相当于ReferenceEquals。(就此而言,这也是Object.equals(..)的意思-不使用任何覆盖。) - ToolmakerSteve

5

如果有人重写了==或!=运算符,他们可以让它们执行任何想要的操作。他们甚至可以让它做一些真正危险的事情,比如return true;return false;。此外,如果有一个重载的运算符,很可能它的性能不如ReferenceEquals(不能保证,但可能并不重要)。

尽管如此,由于任何合理的实现都不太可能出现这种问题。我个人不使用ReferenceEquals,除非我有充分的理由不在该类型或特定实例中使用==运算符。


9
如果重载的 == 运算符实现得不好/不正确,那么应该修复那个 bug。通过使用 ReferenceEquals() 来绕过这个问题并不是正确的方法。 - Michael Burr
4
你认为你能够控制种类并因此有能力修复它。但情况并非总是如此。 - Servy
1
@MichaelBurr,过载的==操作符肯定要修复,但我认为使用ReferenceEquals()是一种可靠的防御性编程,而不是一种变通方法。 - dark_perfect
@ToolmakerSteve 这个问题明确指出是与空字面量进行比较。我从未说过 ReferenceEquals 没有用处,因为 == 应该具有引用语义,只是一个值在合理的实现中只有当它实际上为空时才应该等于 null。 - Servy
好的。我个人不使用ReferenceEquals,除非在该类型或特定实例中使用“==”运算符存在强制性原因,否则会更普遍地避免使用它,并希望澄清您的意思。 - ToolmakerSteve
@ToolmakerSteve 需要在一个已经重载了 == 的类型上执行非空值的引用比较,以获得其他语义,这 使用 ReferenceEquals 而不是 == 的有力理由,因此即使在这种情况下,该语句仍然成立。 - Servy

3
在进行空值检查时,这两个方法应该始终返回相同的结果。如果一个非空引用等于null(即使使用了Null Object模式),无论使用ReferenceEquals还是==运算符,都是一件非常糟糕的事情。因此,在这种情况下,我会使用== /!=运算符。
如果要重载==运算符,则使用ReferenceEquals可能会略微更快。重载==运算符的第一件事就是查看两个变量是否指向同一对象,因此在使用重载运算符的情况下,调用堆栈上会多出一个额外的帧。使用ReferenceEquals还保证这是唯一的检查。
通常,在几乎任何其他情况下,我也会使用== /!=。整个想法是运算符定义“相等”; 这并不总是参考性的(实际上,大多数复合对象应该按结构比较相等; 如果它们的成员相等,则它们是相等的)。理论上,对象最好知道如何将自己与另一个对象进行比较,以获取相等性,相对顺序等等,因此,您应该使用语言的面向对象性质让对象告诉您它是否等于其他任何东西。

实际上,重载通常要做的第一件事情是将 other 进行“尝试转换”为相同的类。如果结果为 null,则已知它不相等。尽管您的观点是正确的:无论进行什么检查都需要一些嵌套调用。 - ToolmakerSteve
整个想法是运算符[你是不是指“类”?]定义“等式”。确切的说,如果你想知道两个变量是否引用同一对象,你不应该使用==,而应该使用ReferenceEquals,因为这就是它的目的。尽管如此,我同意== null是一个特例,可以正常工作。 - ToolmakerSteve

1
我想在这个对话中发表一下意见,即使它已经过时了很久。
假设我们想编写一个检查 null 的扩展方法。我们可以这样做:
public static bool IsNull<T>(this T value) where T : class, new()
{ 
   return value == null;
}

但是这段代码很无聊。

原因是 T 必须是一个类。为什么?因为我们不能将值类型与 null 进行比较。这使得编写良好的通用代码变得非常困难。

或者,您可以将相同的方法附加到对象上,并且永远不必关心该对象是什么。

public static bool IsNull(this object value)
{
   return object.ReferenceEquals(value, null);
}

这打开了其他可能性,例如编写适用于Linq的容错monads等,而不会实际限制您的通用代码。

1
@ToolmakerSteve 我认为你是错误的。在示例中,“this”关键字定义了一个现有类上的扩展方法,此时是“object”类,所有东西都继承自它。上面的方法将起作用,并且不会引发异常。即 string str = null; if (str.IsNull()) // do something 将会工作。编译器将知道字符串类没有“IsNull”函数,但也将知道您已经定义了一个扩展方法。然后编译器调用 IsNull(str),然后调用 object 类上的静态 ReferenceEquals 方法。 - B.O.B.
1
@ToolmakerSteve 附加参考:https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods编辑 PS:我个人喜欢定义扩展方法,用于处理变量为空的情况,因为即使变量为空,我仍然可以调用扩展方法,然后在扩展方法中首先检查空值,处理它,然后执行任何其他简单逻辑,这看起来像是对原始类对象进行函数调用的快捷方式。 - B.O.B.
@B.O.B. - 哎呀,我发誓我几年前测试过了。但是我当时测试的是在扩展方法内部调用this.somefunc(),而不是原始的扩展方法调用value.somefunc(),这才导致了异常。感谢你的纠正! - ToolmakerSteve

1
VB.NET 标签可能不应该包含在这个问题中,因为它没有被提到,但是为了完整起见,Is 等同于 Object.ReferenceEquals,因此始终可以使用它来代替该调用。

0

正如其他答案所解释的那样,这两个调用的语义有些不同。

如果您想使用 ReferenceEquals 的语义,但希望使用简化的语法,在引用类型中,您也可以使用:

if (myObject is object)

0

ReferenceEquals 可能稍微快一点。如前所述,它确保不会调用任何重载的 Equals 运算符。此外,ReferenceEquals 确保只执行一个比较,而不是可能根据重载 Equals 运算符的实现执行 2 次比较。虽然很可能重载运算符本身正在使用 ReferenceEquals 作为第一条语句。

我个人确实使用 ReferenceEquals,但仅在我真正努力挤出最后几个时钟周期的地方使用(这些地方可能每秒被调用数百万次)。或者当我无法控制类型时,例如泛型情况下。... 但再次强调,仅在性能真正关键时使用。


2
请记住,运算符不是虚拟的。重要的是编译时类型,而不是运行时类型。在泛型的情况下,除非有一个约束到一个重载==运算符的类型,否则使用==将始终导致使用object重载,它不需要进行任何检查并简单地调用ReferenceEquals。在这些情况下,它不仅在功能上等效,而且性能几乎相同。这使得泛型成为使用==而不是ReferenceEquals最安全的时间之一。 - Servy
@Servy - 等等,什么?(object) == 不是被定义为调用第一个参数的 Equals 方法吗?在自定义类中定义 Equals 方法的目的不就是为了覆盖这个相等测试吗? - ToolmakerSteve
@ToolmakerSteve 它不仅仅是调用 object.Equals。它将执行 null 检查,如果两者都不为空,则使用虚拟的 object.Equals 来确定结果。由于传入的一个值为 null,我们知道这里不会发生。 - Servy

0
使用C# 9,你可以使用if (myObject is not null)该参考文献指出以下安全保证:

当你将一个表达式与null匹配时,编译器保证不会调用任何用户重载的==或!=运算符。


0

在原始答案被写下十年后,现在正确的做法是两者都不用。

相反,对于null检查,请使用is nullis not null

但是在你的问题正文中,你也问了一个更广泛的问题:

ReferenceEquals==相比有什么好处吗?

巨大的好处是实际上想要地址相等是很少见的。所以我会建议你使用ReferenceEquals来强调,并明确表明这不仅仅是你忘记重载operator==的错误。

实际上,我希望如果你实现了IEquatable<T>并且没有重载operator==,那么C#编译器或IDE会发出警告。或者选择性地发出警告,完全禁止未重载operator==,因为这种情况比我真正想要相同地址的情况要多得多。


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