关于c#:Equals(item,null)或item == null

Equals(item, null) or item == null

使用static object.equals检查空值的代码是否比使用==运算符或正则object.equals的代码更可靠?后两种方法是否容易被重写,以至于检查空值不能按预期工作(例如,当比较值为空时返回false)?

换句话说,这是:

1
if (Equals(item, null)) { /* Do Something */ }

比这个更强大:

1
if (item == null) { /* Do Something */ }

我个人认为后一种语法更容易阅读。在编写将处理作者控制之外的对象(如库)的代码时,应该避免这样做吗?是否应始终避免(在检查空值时)?这只是头发裂开吗?


这个问题没有简单的答案。在我看来,任何一个说总是使用其中一个的人都是在给你不好的建议。

实际上,您可以调用几种不同的方法来比较对象实例。考虑到两个对象实例ab,您可以写:

  • Object.Equals(a,b)
  • Object.ReferenceEquals(a,b)
  • a.Equals(b)
  • a == b

这些都可以做不同的事情!

默认情况下,Object.Equals(a,b)将对引用类型执行引用相等比较,对值类型执行位比较。来自msdn文档:

The default implementation of Equals
supports reference equality for
reference types, and bitwise equality
for value types. Reference equality
means the object references that are
compared refer to the same object.
Bitwise equality means the objects
that are compared have the same binary
representation.

Note that a derived type might
override the Equals method to
implement value equality. Value
equality means the compared objects
have the same value but different
binary representations.

注意上面最后一段…我们稍后再讨论。

Object.ReferenceEquals(a,b)只执行引用相等比较。如果传递的类型是装箱值类型,则结果始终为false

a.Equals(b)调用Object的虚拟实例方法,a的类型可以重写该方法来执行它想要的任何操作。调用是使用虚拟调度执行的,因此运行的代码取决于a的运行时类型。

a == b调用a的**编译时类型*的静态重载运算符。如果该运算符的实现在ab上调用实例方法,则它还可能取决于参数的运行时类型。由于分派基于表达式中的类型,因此以下内容可能会产生不同的结果:

1
2
3
4
5
6
7
Frog aFrog = new Frog();
Frog bFrog = new Frog();
Animal aAnimal = aFrog;
Animal bAnimal = bFrog;
// not necessarily equal...
bool areEqualFrogs = aFrog == bFrog;
bool areEqualAnimals = aAnimal = bAnimal;

因此,是的,使用operator ==检查空值存在漏洞。在实践中,大多数类型都不会使==过载,但从来没有保证。

实例方法Equals()在这里并不好。当默认实现执行引用/位相等检查时,类型可以重写Equals()成员方法,在这种情况下,将调用此实现。用户提供的实现可以返回所需的任何内容,即使与空值比较也是如此。

但是,你问的静态版本的Object.Equals()呢?这最终会运行用户代码吗?原来答案是肯定的。Object.Equals(a,b)的实施扩展到以下方面:

1
((object)a == (object)b) || (a != null && b != null && a.Equals(b))

您可以自己尝试:

1
2
3
4
5
6
class Foo {
    public override bool Equals(object obj) { return true; }  }

var a = new Foo();
var b = new Foo();
Console.WriteLine( Object.Equals(a,b) );  // outputs"True!"

因此,当调用中的两种类型都不是null时,语句Object.Equals(a,b)可以运行用户代码。注意,当其中一个参数为空时,Object.Equals(a,b)不调用Equals()的实例版本。

简而言之,根据您选择调用的方法,您得到的比较行为类型可能会有很大的不同。然而,这里有一条评论:微软没有正式记录Object.Equals(a,b)的内部行为。如果您需要一个铁壳Gaurantee,在不运行任何其他代码的情况下将引用与空值进行比较,则需要Object.ReferenceEquals()

1
Object.ReferenceEquals(item, null);

这种方法的目的非常明确——您特别希望结果是两个引用的比较,以确保引用相等。这里使用Object.Equals(a,null)这样的工具的好处在于,不太可能有人会晚些时候过来说:

"嘿,这很尴尬,我们换成:a.Equals(null)a == null"

可能会有所不同。

不过,让我们在这里注入一些实用主义。到目前为止,我们已经讨论了不同比较方式产生不同结果的可能性。虽然确实如此,但在某些类型中,编写a == null是安全的。像StringNullable这样的内置.NET类有很好的语义来进行比较。此外,他们是sealed——防止通过继承改变他们的行为。以下是非常常见的(正确的):

1
2
string s = ...
if( s == null ) { ... }

没有必要(也很难看)写:

1
if( ReferenceEquals(s,null) ) { ... }

因此,在某些有限的情况下,使用==是安全和适当的。


if (Equals(item, null))并不比if (item == null)更健壮,而且我发现启动起来更令人困惑。


当您想测试身份(内存中的相同位置)时:

ReferenceEquals(a, b)

处理空值。不可重写。100%保险箱。

但要确保你真的想要身份测试。考虑以下内容:

ReferenceEquals(new String("abc"), new String("abc"))

返回false。相反:

Object.Equals(new String("abc"), new String("abc"))

(new String("abc")) == (new String("abc"))

两者都返回true

如果您希望在这种情况下得到true的答案,那么您需要一个相等测试,而不是身份测试。见下一部分。

当要测试相等性(相同内容)时:

  • 如果编译器没有抱怨,请使用"a == b"。

  • 如果被拒绝(如果变量A的类型没有定义"=="运算符),则使用"Object.Equals(a, b)"。

  • 如果您在逻辑内部,其中a不为空,那么您可以使用更可读的"a.Equals(b)"。例如,"this.equals(b)"是安全的。或者如果"a"是在构造时初始化的字段,并且如果将空值作为要在该字段中使用的值传入,则构造函数将引发异常。

现在,要解决原始问题:

Q: Are these susceptible to being overridden in some class, with code that does not handle null correctly, resulting in an exception?

是的。获得100%安全等同性测试的唯一方法是自己预先测试空值。

但你应该吗?这个bug将在那个(假设的未来坏类)中,并且它将是一个直接的失败类型。易于调试和修复(由提供类的任何人提供)。我怀疑这是一个经常发生的问题,或者当它确实发生时会持续很长时间。

更详细的A:Object.Equals(a, b)最有可能面对写得不好的班级。如果"a"为空,则对象类将自己处理它,因此没有风险。如果"b"为空,则"a"的动态(运行时而非编译时)类型确定调用什么"等于"方法。当"b"为空时,被调用的方法只需正常工作即可。除非被调用的方法编写得非常糟糕,否则它所做的第一步就是确定"b"是否是它理解的类型。

因此,Object.Equals(a, b)是可读性/编码工作和安全性之间的合理折衷。


框架指南建议您将Equals视为值相等(检查两个对象是否代表相同的信息,即比较属性),将==视为引用相等(不可变对象除外),对此,您可能应将==视为值相等。

因此,假设这些指导原则适用于这里,选择语义上合理的。如果您处理的是不可变的对象,并且您希望这两种方法产生相同的结果,那么为了清晰起见,我将使用==


关于"…编写处理作者控制之外的对象的代码…",我将指出静态Object.Equals==运算符都是静态方法,因此不能被虚拟/重写。在编译时根据静态类型确定调用哪个实现。换句话说,外部库无法为编译后的代码提供不同版本的例程。