为什么(object)0 ==(object)0与((object)0)。等于((object)0)不同?


117

为什么以下表达式不同?

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

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


36
好,现在您已经知道了该问题的答案,请通过预测以下结果来检查您的理解:short myShort = 0; int myInt = 0; Console.WriteLine("{0}{1}{2}", myShort.Equals(myInt), myInt.Equals(myShort), myInt == myShort); 现在对照实际情况进行检查。您的预测正确吗?如果没有,您能解释这个差异吗?
埃里克·利珀特

1
@Star,有关推荐阅读,请参见msdn.microsoft.com/en-us/library/vstudio/…,了解int16aka shortEquals方法的可用重载,然后查看msdn.microsoft.com/en-us/library/ms173105.aspx。我不想破坏埃里克·利珀特(Eric Lippert)的困惑,但是一旦阅读这些页面,就应该很容易弄清楚。
Sam Skuce

2
我以为这是一个Java问题。至少在看到“ E”等于“ E”之前。
seteropere 2013年

4
@seteropere Java实际上是不同的:Java中的自动装箱会缓存对象,因此((Integer)0)==((Integer)0)评估为true。
2013年

1
您也可以尝试IFormattable x = 0; bool test = (object)x == (object)x;。当结构已经在盒子中时,不执行新的装箱。
Jeppe Stig Nielsen 2013年

Answers:


151

调用行为不同的原因是它们绑定到非常不同的方法。

==案例将绑定到静态引用相等运算符。int创建了2个独立的框式值,因此它们不是相同的引用。

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


==情况下,不调用Object.ReferenceEquals。它只是产生ceqIL指令以执行参考比较。
山姆·哈威尔

8
@ 280Z28不仅仅是因为编译器内联吗?
markmnl

@ 280Z28那么?类似的情况是,它们的Boolean.ToString方法显然在其函数内包含硬编码的字符串,而不是返回公开公开的Boolean.TrueString和Boolean.FalseString。没关系 关键是,==做与ReferenceEquals(无论如何在对象上)相同的事情。只是在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 2013年

@JaredPar:规范说的很有趣,因为那不是语言的实际行为。给定如上定义的运算符和变量Cat Whiskers; Dog Fido; IDog Fred;(用于非相关接口ICatIDog非相关类Cat:ICatand Dog:IDog),则比较Whiskers==FidoWhiskers==34将是合法的(仅当Whiskers和Fido都为null时,第一个才是正确的;第二个永远不能为真) )。实际上,C#编译器会同时拒绝这两种方法。 Whiskers==Fred;如果Cat是密封的,将被禁止,如果没有密封,则将被允许。
超级猫

26

当将int值0(或任何其他值类型)转换为时object,该值将被装箱。每个转换object产生一个不同的框(即一个不同的对象实例)。该类型的==运算符object执行引用比较,因此它返回false,因为左侧和右侧不是同一实例。

另一方面,当使用Equals虚拟方法时,它将使用实际盒装类型的实现,即Int32.Equals,由于两个对象具有相同的值,因此返回true。


18

==静态的操作员不是虚拟的。它将运行object该类定义的确切代码(“ object是操作数的编译时类型”),无论哪个对象的运行时类型如何,它都会进行引用比较。

Equals方法是虚拟实例方法。它将运行在(第一个)对象的实际运行时类型中定义的代码,而不是object类中的代码。在这种情况下,对象是int,因此它将执行值比较,因为这是int类型为其Equals方法定义的内容。


==令牌实际上代表了两家运营商,其中之一是重载以及其中之一是没有。第二个运算符的行为与(对象,对象)的重载行为有很大不同。
2013年

13

Equals()方法是虚拟的。
因此,即使将调用网站强制转换为,它也始终调用具体实现objectint覆盖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#引用相等运算符(通过使用==未定义重载的on操作数类型表示)使用特殊指令而不是调用ReferenceEquals。此外,Object.ReferenceEquals将接受仅在两个碰巧都为null的情况下才能匹配的操作数,并且将接受需要类型强制转换Object从而因此不可能匹配任何内容的操作数,而的引用相等版本==将拒绝编译此类用法。
2013年

9

C#运算符使用令牌==表示两个不同的运算符:一个静态可重载的比较运算符和一个不可重载的参考比较运算符。当遇到==令牌时,它首先检查是否存在适用于操作数类型的相等测试重载。如果是这样,它将调用该重载。否则,它将检查类型是否适用于参考比较运算符。如果是这样,它将使用该运算符。如果两个运算符都不适用于操作数类型,则编译将失败。

与所有值类型一样,代码(Object)0不仅将Int32to转换为ObjectInt32,实际上还代表两种类型,其中一种描述值和存储位置(例如文字零),但不派生自任何东西,其中一种描述堆对象并从中派生Object; 因为只有后一种类型可以被转换为Object,所以编译器必须创建该后一种类型的新堆对象。每次调用(Object)0都会创建一个新的堆对象,因此这两个操作数==是不同的对象,每个对象独立封装Int32值0。

该类Object没有为equals运算符定义任何可用的重载。因此,编译器将无法使用重载的等式测试运算符,而将转而使用参考等式测试。由于这两个操作数==引用不同的对象,因此将报告false。第二个比较成功,因为它询问一个堆对象实例Int32是否等于另一个。因为该实例知道等于另一个不同的实例意味着什么,所以它可以回答true


此外,每次您0在代码中编写文字时,我都会假定它为此在堆中创建了一个int对象。它不是对一个全局静态零值的唯一引用(例如,它们如何使String.Empty避免仅为了初始化新字符串而制作新的空字符串对象),因此我很确定即使执行a 0.ReferenceEquals(0)也会返回false,因为两个0都是新创建的Int32对象。
Nyerguds

1
@Nyerguds,我很确定您说的所有内容都不正确,因为int,堆,历史记录,全局静态变量等都 0.ReferenceEquals(0)将失败,因为您正试图在编译时间常数上调用方法。没有对象可以将其挂起。未装箱的int是存储在堆栈中的结构。甚至int i = 0; i.ReferenceEquals(...)都行不通。因为System.Int32不继承自Object
Andrew Backer

@AndrewBacker System.Int32是a struct,是structis System.ValueType,它本身是继承的System.Object。那就是为什么有一种ToString()方法和一种Equals方法System.Int32
塞巴斯蒂安

1
但是,Nyerguds错误地指出将在堆上创建一个Int32,但事实并非如此。
塞巴斯蒂安

@SebastianGodelet,我有点无视内部。System.Int32本身实现了这些方法。GetType()在上是extern Object,那是很久以前我就不再为此担心的地方。从来没有必要走得更远。AFAIK CLR对这两种类型的处理方式不同,特别是。不只是继承。它is是两种数据类型之一。我只是不希望任何人阅读该评论并走得太远,包括对空字符串的陌生感,而它忽略了字符串插入。
Andrew Backer

3

两种检查不同。第一个检查身份,第二个检查是否平等。通常,如果两个术语指的是同一对象,则它们是相同的。这意味着它们是平等的。如果两个术语的值相同,则它们相等。

在编程方面,通常通过引用相等来破坏身份。如果两个术语的指针相等(!),则它们所指向的对象完全相同。但是,如果指针不同,则它们指向的对象的值仍可以相等。在C#中,可以使用静态Object.ReferenceEquals成员检查身份,而使用非静态Object.Equals成员检查是否相等。由于您要将两个整数转换为对象(称为“装箱”,btw),因此操作符==object执行第一个检查,默认情况下将进行第一次映射Object.ReferenceEquals并检查其身份。如果您显式调用non-static Equals-member,则动态调度将导致对的调用Int32.Equals,以检查是否相等。

这两个概念相似,但不相同。首先,它们看起来可能令人困惑,但是细微的差异非常重要!想象两个人,即“爱丽丝”和“鲍勃”。他们俩都住在一个黄色的房子里。根据这样的假设,爱丽丝和鲍勃住在一个只有颜色不同的房屋的地区,他们俩可能都住在不同的黄色房屋中。如果比较两个房屋,您会发现它们是绝对相同的,因为它们都是黄色的!但是,他们没有共享同一个房屋,因此他们的房屋是平等的,但并不相同。身份表明他们住在同一所房子里。

注意:某些语言定义了===操作员以检查身份。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.