为什么(对象)0 == (对象)0 与 ((对象)0).Equals((对象)0) 不同?

117

以下表达式为什么不同?

[1]  (object)0 == (object)0 //false
[2]  ((object)0).Equals((object)0) // true

实际上,我完全可以理解[1],因为可能.NET运行时会将整数装箱,从而开始比较引用。但是,为什么[2]不同呢?

其实,我完全理解[1],因为可能.NET运行时会将整数装箱,导致开始比较引用。但是,为什么[2]不同呢?


36
现在你已经理解了这个问题的答案,通过预测以下代码的结果来检验一下自己的理解:short myShort = 0; int myInt = 0; Console.WriteLine("{0}{1}{2}", myShort.Equals(myInt), myInt.Equals(myShort), myInt == myShort); 现在将其与实际结果进行比较。你的预测是否正确?如果不是,你能解释其中的差异吗? - Eric Lippert
1
@Star,推荐阅读http://msdn.microsoft.com/en-us/library/vstudio/system.int16.equals%28v=vs.100%29.aspx,了解`int16`又称为`short` Equals方法的可用重载,然后查看http://msdn.microsoft.com/en-us/library/ms173105.aspx。我不想破坏Eric Lippert的谜题,但是一旦你阅读了这些页面,应该很容易弄清楚。 - Sam Skuce
2
在看到“Equals”中的“E”之前,我认为这是一个Java问题。 - seteropere
4
Java与其他语言不同:Java中的自动装箱会缓存对象,因此((Integer)0)==((Integer)0)的结果为true。 - Jules
1
你也可以尝试 IFormattable x = 0; bool test = (object)x == (object)x;。当结构体已经在盒子里时,不会执行新的装箱操作。 - Jeppe Stig Nielsen
显示剩余3条评论
7个回答

151

这些调用表现不同的原因在于它们绑定到非常不同的方法。

== 情况将绑定到静态引用相等运算符。由此创建了2个独立的装箱 int 值,因此它们不是同一个引用。

在第二种情况下,您将绑定到实例方法 Object.Equals。这是一个虚拟方法,它将过滤到 Int32.Equals 并检查是否有一个装箱整数。两个整数值都是0,因此它们是相等的。


== 情况并不会调用 Object.ReferenceEquals。它只是生成 ceq IL 指令来执行引用比较。 - Sam Harwell
8
@280Z28 这不仅仅是因为编译器将其内联(inline)了吗? - markmnl
@280Z28 那又怎样?类似的情况是,他们的 Boolean.ToString 方法显然在其函数内包含硬编码字符串,而不是返回公开暴露的 Boolean.TrueString 和 Boolean.FalseString。这并不重要;关键是 ==ReferenceEquals(至少在 Object 上)执行相同的操作。这只是 MS 内部优化的一部分,以避免在经常使用的函数上进行不必要的内部函数调用。 - Nyerguds
6
C#语言规范第7.10.6段表示: 预定义的引用类型相等运算符为: bool operator ==(object x, object y); bool operator !=(object x, object y); 这些运算符返回比较两个引用是否相等或不相等的结果。并不要求使用System.Object.ReferenceEquals 方法来确定结果。至于@markmnl的问题:不,C#编译器不会执行内联优化,这是即时编译器有时执行的操作(但在这种情况下不会)。所以280Z28是正确的,ReferenceEquals方法实际上并没有被使用。 - Jeppe Stig Nielsen
@JaredPar:有趣的是规范这样说,因为这不是语言实际的行为。假设操作符定义如上,并且变量Cat Whiskers; Dog Fido; IDog Fred;(对于非相关接口ICatIDog以及非相关类Cat:ICatDog:IDog),比较Whiskers==FidoWhiskers==34是合法的(如果Whiskers和Fido都为空,则第一个比较只能为真;第二个比较永远不可能为真)。事实上,C#编译器将拒绝这两个比较。如果Cat是密封的,则Whiskers==Fred;将被禁止,但如果它没有被密封,则允许。 - supercat

26
当您将int值0(或任何其他值类型)转换为object时,该值会被装箱。每次向object进行强制转换都会产生一个不同的盒子(即不同的对象实例)。object类型的==运算符执行引用比较,因此它返回false,因为左侧和右侧不是同一个实例。
另一方面,当您使用Equals方法(这是一个虚拟方法)时,它使用实际装箱类型的实现,即Int32.Equals,因为两个对象具有相同的值,所以它返回true。

18
==运算符是静态的,不是虚拟的。它将运行object类定义的完全相同的代码(其中object是操作数的编译时类型),这将执行引用比较,而不考虑任何对象的运行时类型。 Equals方法是一个虚拟实例方法。它将运行在(第一个)对象的实际运行时类型中定义的代码,而不是object类中的代码。在这种情况下,对象是一个int,因此它将执行值比较,因为这是int类型为其Equals方法定义的内容。

==符号实际上代表两个运算符,其中一个可以重载,而另一个则不能。第二个运算符的行为与(object, object)上的重载非常不同。 - supercat

13

Equals() 方法是虚拟的。
因此,即使调用方被强制转换为 object,它也总是调用具体实现。 int 通过值进行比较来重写 Equals(),因此您获得值比较。


10

== 使用:Object.ReferenceEquals

Object.Equals 比较值。

object.ReferenceEquals 方法比较引用。当您分配一个对象时,您会收到一个包含值的引用,该值指示其内存位置以及内存堆中对象的数据。

object.Equals 方法比较对象的内容。它首先检查引用是否相等,就像 object.ReferenceEquals 一样。但然后它调用派生的 Equals 方法进一步测试相等性。 请参见:

   System.Object a = new System.Object();
System.Object b = a;
System.Object.ReferenceEquals(a, b);  //returns true

虽然 Object.ReferenceEquals 的行为类似于在其操作数上使用 C# == 运算符的方法,但 C# 引用相等运算符(表示为对没有定义重载的操作数类型使用 ==)使用特殊指令而不是调用 ReferenceEquals。此外,Object.ReferenceEquals 将接受只有当两个操作数都恰好为 null 时才能匹配的操作数,并将接受需要强制转换为 Object 的操作数,因此不能匹配任何内容,而 == 的引用相等版本则会拒绝编译这种用法。 - supercat

9
C#运算符使用标记==代表两种不同的运算符:可以静态重载的比较运算符和不可重载的引用比较运算符。当它遇到==标记时,首先检查是否存在适用于操作数类型的任何等式测试重载。如果有,则调用该重载。否则,它将检查类型是否适用于引用比较运算符。如果是,则使用该运算符。如果没有运算符适用于操作数类型,则编译失败。
代码(Object)0不仅仅是将Int32向上转型为ObjectInt32,像所有值类型一样,实际上表示两种类型,一种描述值和存储位置(例如字面量零),但不从任何内容派生,另一种描述堆对象并从Object派生;因为只有后者类型可以向上转型为Object,所以编译器必须创建一个新的堆对象。每次调用(Object)0都会创建一个新的堆对象,因此==的两个操作数是不同的对象,每个对象都独立地封装Int32值0。
Object没有定义任何可用的等于运算符重载。因此,编译器将无法使用重载的等式测试运算符,并将退回到使用引用等式测试。因为==的两个操作数引用不同的对象,所以它会报告false。第二个比较成功是因为它询问一个Int32堆对象实例是否等于另一个。因为该实例知道与另一个不同实例相等意味着什么,它可以回答true

1
@Nyerguds,我非常确定你所说的关于ints、heap、history、global statics等都是不正确的。0.ReferenceEquals(0)会失败,因为你试图在编译时常量上调用一个方法。没有对象可以挂载它。未装箱的int是一个结构体,存储在堆栈上。即使int i = 0; i.ReferenceEquals(...)也不起作用。因为System.Int32不继承自Object - Andrew
1
然而,Nyerguds 的说法是错误的,Int32 不会被创建在堆上,这并不是事实。 - Sebastian
@SebastianGodelet,我在这方面有点忽略内部细节。System.Int32本身实现了这些方法。GetType()在Object上是外部的,那就是我很久以前停止担心它的地方。没有必要再深入了解。据我所知,CLR以不同的方式和特别处理这两种类型。这不仅仅是继承。它两种数据类型之一。我只是不想让任何人阅读该评论并偏离轨道,包括关于空字符串的奇怪性质,这忽略了字符串插值。 - Andrew
比我能够给出的更明确的解释。我明白了,但我无法清楚地表达:https://dev59.com/f2nWa4cB1Zd3GeqP263a - Andrew
这里有一篇来自 Erric Lippert 的非常全面的答案: https://dev59.com/RXE85IYBdhLWcg3w430X#2561622 - Sebastian
显示剩余5条评论

3

这两个检查是不同的。第一个检查的是身份,第二个检查的是相等性。一般来说,如果两个术语指的是同一个对象,则它们是相同的。这意味着它们是相等的。如果两个术语的值相同,则它们是相等的。

在编程方面,身份通常通过引用相等性来衡量。如果两个术语的指针相等(!),则它们指向的对象完全相同。但是,如果指针不同,则它们所指向的对象的值仍然可以相等。在C#中,可以使用静态的Object.ReferenceEquals成员来检查身份,而可以使用非静态的Object.Equals成员来检查相等性。由于您将两个整数转换为对象(顺便说一下,这称为“装箱”),因此object==操作符执行第一个检查,默认情况下映射到Object.ReferenceEquals并检查身份。如果您明确调用非静态的Equals-成员,则动态分派会导致调用Int32.Equals,该方法检查相等性。

两个概念相似但不同。对于初学者来说,它们可能很混淆,但小差别非常重要!想象两个人,名为“Alice”和“Bob”。他们都住在一栋黄色房子里。基于这样的假设,即Alice和Bob生活在一个区域内,房屋只有颜色不同,他们可以住在不同的黄色房子里。如果你比较这两个房子,你会发现它们完全相同,因为它们都是黄色的!然而,它们不是同一所房子,因此它们的房子是相等的,但不是相同的。相同意味着他们住在同一所房子里。
注意:有些语言定义“===”运算符来检查身份。

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