使用static object.equals检查空值的代码是否比使用==运算符或正则object.equals的代码更可靠?后两种方法是否容易被重写,以至于检查空值不能按预期工作(例如,当比较值为空时返回false)?
换句话说,这是:
1
| if (Equals(item, null)) { /* Do Something */ } |
比这个更强大:
1
| if (item == null) { /* Do Something */ } |
我个人认为后一种语法更容易阅读。在编写将处理作者控制之外的对象(如库)的代码时,应该避免这样做吗?是否应始终避免(在检查空值时)?这只是头发裂开吗?
这个问题没有简单的答案。在我看来,任何一个说总是使用其中一个的人都是在给你不好的建议。
实际上,您可以调用几种不同的方法来比较对象实例。考虑到两个对象实例a和b,您可以写:
- 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的**编译时类型*的静态重载运算符。如果该运算符的实现在a或b上调用实例方法,则它还可能取决于参数的运行时类型。由于分派基于表达式中的类型,因此以下内容可能会产生不同的结果:
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是安全的。像String和Nullable这样的内置.NET类有很好的语义来进行比较。此外,他们是sealed——防止通过继承改变他们的行为。以下是非常常见的(正确的):
1 2
| string s = ...
if( s == null ) { ... } |
没有必要(也很难看)写:
1
| if( ReferenceEquals(s,null) ) { ... } |
因此,在某些有限的情况下,使用==是安全和适当的。
- 我认为我的答案(微软的代理回答)是一个相当简单的答案。
- 像"我应该/永远不应该做X"这样的问题意味着对所讨论主题细微差别的知识差距。我觉得这里再详细一点会有助于澄清为什么我认为一个简单的答案没有意义。
- 实际上,静态object.Equals(a,b)方法可以调用对象a上的重载/虚拟Equals方法。如果我记得正确的话,它的作用类似于((object)a == (object)b) || (a != null && b != null && a.Equals(b))。(与手术室询问的null相比,这无关紧要,但与一般情况有关。)
- @卢克:我增加了一些叙述来讨论这个案子。我最初只关注Instace-to-null的比较,但这也值得解释。感谢您的反馈。
- 非常有用的答案。然而,在讨论字符串(以及任何其他内部是引用但逻辑上充当值的类型)时,它混淆了referenceEquals和object.equals之间的区别。说"写referenceequals(mystring,空值)是不必要和丑陋的",忽略了这样做是完全错误的事实。
- imho,在实践中,你应该使用"a==b",除非你知道你想要参考比较。如果类型不支持"=="运算符,则使用object.equals(a,b)。我建议不要使用"a.equals(b)",因为如果a为空,则会导致异常。如果要实现一个未能正确处理object.equals的类型,那将是该实现中的错误,而不是您使用它时的错误。不要为了避免可能有人写了一个糟糕的班级而去做扭曲。
- 有趣的是,Visual Studio建议将referenceEquals(foo,null)转换为"foo is null"
if (Equals(item, null))并不比if (item == null)更健壮,而且我发现启动起来更令人困惑。
- 有人可以在不覆盖Equals的情况下重载==运算符。这将是一个糟糕的设计,但完全有可能使平等比较的语义在两者之间有所不同。另外,如果==操作符被重载,它可能会与内置的Objects.Equals()方法做一些完全不同的事情,我认为这只是检查引用的相等性。
- @lbushkin如果你对你检查的对象如此偏执,那么(null==obj)不能保证引用比较吗?即使==被重载,根据定义,结果将是预期的行为。如果这不是预期的行为,那么这是一个超出了这个问题范围的问题。
当您想测试身份(内存中的相同位置)时:
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视为值相等(检查两个对象是否代表相同的信息,即比较属性),将==视为引用相等(不可变对象除外),对此,您可能应将==视为值相等。
因此,假设这些指导原则适用于这里,选择语义上合理的。如果您处理的是不可变的对象,并且您希望这两种方法产生相同的结果,那么为了清晰起见,我将使用==。
- 微软的指导方针没有解决空值的问题,这就是问题所在。最重要的是,"mystring==null"是一个安全的测试,"mystring.equals(null)"将在mystring为空时引发异常。此外,微软的指导方针甚至没有提到object.equals(myObject,b)和myObject.equals(b)之间的区别。前者是健壮的;如果myObject为空,后者则给出异常。因此,虽然您提供的链接很有用,但它不是海报问题的答案。
关于"…编写处理作者控制之外的对象的代码…",我将指出静态Object.Equals和==运算符都是静态方法,因此不能被虚拟/重写。在编译时根据静态类型确定调用哪个实现。换句话说,外部库无法为编译后的代码提供不同版本的例程。
- 这句话不完全正确。在类型上实现操作符==可以在其中一个涉及的实例上调用虚方法。这意味着实际上可能发生的事情不是基于表达式中的类型,而是基于所涉及的运行时类型。
- 静态object.Equals(a,b)方法也同样适用,它的作用类似于((object)a == (object)b) || (a != null && b != null && a.Equals(b)),也就是说,它可以在对象a上调用虚拟Equals方法。
- @你的观点是有效的,但是这并不等于(请原谅双关语)我的说法是"不完全正确"。
- 在语句"外部库无法为编译代码提供不同版本的例程"之前,我一直支持您。正如我在评论中提到的,以及他的@lukeh中提到的,虚拟方法可以作为object.Equals(a,b)或a == b的结果运行。
- @lbushkin:我们在这里讨论语义,但是只要我们在讨论:-):我说的是例程,即静态对象.equals或"=="运算符。例程所采用的执行路径可能受到许多因素的影响,包括,是的,上述例程使用虚拟方法。当然,只有在编译时确定的例程实际使用虚拟方法时,这种不太可能发生的情况才会发生。
- @丹尼尔+1谢谢。这在微软的设计中是一个非常微妙的错误。我刚刚验证了"=="在编译时类型更一般的情况下,无法调用子类的版本。但是请注意,"object.equals"是对象类中的静态方法,如果存在,它会成功调用mysubclass.equals(object other)方法。读者应该知道,如果存在iequatable版本mysubclass.equals(mysubclass other),它可能不会直接调用它。因此,在适当的时候,必须编写通用的(对象-其他)来调用特定的(mysubclass-其他)。
- @lbushkin:如果一个类型重写Equals(Object),静态Object.Equals(Object,Object)方法将在其中一个对象上调用该重写,除非另一个对象为空;无论其参数如何类型化,这都会起作用。相反,即使类型重载==,它也不能保证重载在强制转换为Object时使用。