对于C#中的基元,==和Equals()之间有什么区别?


180

考虑以下代码:

int age = 25;
short newAge = 25;
Console.WriteLine(age == newAge);  //true
Console.WriteLine(newAge.Equals(age)); //false
Console.ReadLine();

这两个intshort是原始类型,但有一个比较==返回true,并用比较Equals返回false。

为什么?


9
@OrangeDog请考虑问题,然后投票关闭

4
这缺少明显的反向尝试:Console.WriteLine(age.Equals(newAge));
ANeves 2014年

3
重复项无法解释此行为;这只是关于Equals()一般情况。
SLaks 2014年

37
我几天前在Coverity博客上回答了这个确切的问题。blog.coverity.com/2014/01/13/inconsistent-equality
Eric Lippert

5
@CodesInChaos:规范实际上两次使用了“原始类型”一词,却从未对其进行定义;这意味着基元类型是内置的值类型,但这从未明确。我向Mads建议将该术语从规范中剔除,因为它似乎造成了比消除它更混乱的情况。
埃里克·利珀特

Answers:


262

简短答案:

平等是复杂的。

详细答案:

基本类型将覆盖基数,object.Equals(object)并且如果装箱object类型和值相同,则返回true 。(请注意,它也适用于可为null的类型;非null的可为null的类型始终框住基础类型的实例。)

因为newAge是a short,所以Equals(object)如果您传递具有相同值的装箱的short,则其方法仅返回true 。您传递的是boxed int,因此它返回false。

相反,将==运算符定义为取两个ints(或shorts或longs)。
当您使用int和调用它时short,编译器将隐式将转换为shortint并按int值比较结果s。

其他使其运作的方式

基本类型也有自己的Equals()接受相同类型的方法。
如果编写age.Equals(newAge),编译器将选择int.Equals(int)最佳重载并将其隐式转换shortint。然后它将返回true,因为此方法只是int直接比较。

short也有一个short.Equals(short)方法,但是int不能隐式转换为short,因此您没有调用它。

您可以强制其使用强制转换调用此方法:

Console.WriteLine(newAge.Equals((short)age)); // true

这将short.Equals(short)直接调用,无需装箱。如果age大于32767,则将引发溢出异常。

您也可以调用short.Equals(object)重载,但显式传递一个装箱的对象,使其具有相同的类型:

Console.WriteLine(newAge.Equals((object)(short)age)); // true

与之前的替代方法一样,如果不能容纳,则会引发溢出short。与以前的解决方案不同,它将装箱short到一个对象中,浪费时间和内存。

源代码:

这是Equals()来自实际源代码的两种方法:

    public override bool Equals(Object obj) {
        if (!(obj is Int16)) {
            return false;
        }
        return m_value == ((Int16)obj).m_value;
    }

    public bool Equals(Int16 obj)
    {
        return m_value == obj;
    }

进一步阅读:

参见埃里克·利珀特


3
@SLaks,如果我们调用long == int,则int隐式转换为long正确吗?
SelmanGenç14年

1
是的,我没有实际尝试就写下了所有内容。
SLaks 2014年

1
请记住,在问题代码中,如果将更int age = 25;改为const int age = 25;,则结果将更改。那是因为在这种情况下确实存在从int到的隐式转换short。请参见隐式常量表达式转换
Jeppe Stig Nielsen 2014年

2
@SLaks是的,但是您的答案“传递的值”的措词可以双向解释(作为开发人员传递的值,或拆箱后CLR实际传递的值)。我猜测还不知道这里答案的临时用户将其读为原用户
JaredPar 2014年

2
@Rachel:除了那不是真的;该默认 ==操作者通过参考比较引用类型。对于值类型,对于过载的类型,==不是。
SLaks 2014年

55

因为没有重载,short.Equals所以接受int。因此,这称为:

public override bool Equals(object obj)
{
    return obj is short && this == (short)obj;
}

obj不是short..因此,它是错误的。


12

当你传递intshort“S等于你通过object

在此处输入图片说明 因此,此伪代码运行:

return obj is short && this == (short)obj;


10

==用于检查相等条件,可以将其视为一个运算符(布尔运算符),仅用于比较两件事,此处的数据类型无关紧要,因为将进行类型转换,Equals也用于检查相等条件,但是在这种情况下,数据类型应该相同。N等于不是操作符。

下面是从您提供的示例中摘录的一个小示例,这将简要说明不同之处。

int x=1;
short y=1;
x==y;//true
y.Equals(x);//false

在上面的示例中,X和Y具有相同的值,即1,并且当我们使用时==,它将返回true,就像在的情况下一样==,short类型由编译器转换为int并给出了结果。

当我们使用时Equals,比较完成,但是编译器未完成类型转换,因此返回false。

伙计们,如果我错了,请告诉我。


6

在方法或运算符参数不是必需类型的许多情况下,C#编译器将尝试执行隐式类型转换。如果编译器可以通过添加隐式转换使所有参数都满足其运算符和方法,那么即使在某些情况下(尤其是使用相等性测试!),编译器也可以毫无抱怨地这样做。

此外,每个值类型,例如intshort实际上都描述了一种值和一种object(*)。存在隐式转换,可以将值转换为其他类型的值,以及将任何类型的值转换为其对应类型的对象,但是不同类型的对象不能彼此隐式转换。

如果使用==运算符比较a short和an intshort则将隐式转换为an int。如果其数值等于的数值int,则将int其转换int为与之比较的数值。但是,如果有人尝试使用Equalsshort上的方法将其与进行比较int,则唯一可以满足该Equals方法重载的隐式转换将是对的对象类型的转换int。当short询问s是否与传入的对象匹配时,它将观察到所讨论的对象是a int而不是a short,因此得出结论,它不可能相等。

通常,尽管编译器不会抱怨它,但是应该避免比较不属于同一类型的东西。如果有人对将事物转换为通用形式是否会产生相同的结果感兴趣,则应明确执行这种转换。考虑例如

int i = 16777217;
float f = 16777216.0f;

Console.WriteLine("{0}", i==f);

您可能会通过三种方式将a int与a 进行比较float。一个人可能想知道:

  1. 是否最接近匹配的float值?intfloat
  2. float匹配的整数部分是否匹配int
  3. intfloat表示相同的数值。

如果尝试直接比较intfloat,则编译后的代码将回答第一个问题;然而,这是否是程序员的意图还远不是显而易见的。将比较更改为(float)i == f可以清楚地表明第一个含义是预期的,或者(double)i == (double)f将导致代码回答第三个问题(并明确表明这是预期的)。

(*)即使C#规范将类型的值System.Int32视为例如类型的对象System.Int32,但这种观点与代码运行在其规范将值和对象视为居住在不同Universe的平台上的要求相矛盾。此外,如果T是引用类型,并且xT,则该类型的引用T应该能够引用x。因此,如果vtype 的变量Int32包含,则type Object的引用Object应该能够保存v对它或其内容的引用。实际上,类型的引用Object将能够指向一个对象,该对象保存有从复制的数据v,而不指向v其自身或其内容。这将表明v它的内容也不是真的Object


1
the only implicit conversion which would satisfy an overload of the Equals method would be the conversion to the object type corresponding to int错误。与Java不同,C#没有单独的原始类型和装箱类型。被装箱,object因为这是的唯一其他重载Equals()
SLaks

第一个和第三个问题是相同的;转换为时,确切的值已经丢失float。将a 强制转换float为a double不会神奇地创造新的精度。
SLaks

@SLaks:根据描述C#运行的虚拟机的ECMA规范,每个值类型定义都会创建两个不同的类型。C#规范可能会说类型的存储位置的内容和类型List<String>.Enumerator的堆对象的内容List<String>.Enumerator是相同的,但是ECMA / CLI规范说它们是不同的,即使在C#中使用时,它们的行为也有所不同。
2014年

@SLaks:如果在比较之前将if分别转换为double,它们将产生16777217.0和16777216.0,它们比较起来不相等。转换为,i float将得出16777216.0f,相比之下f
2014年

@SLaks:有关存储位置类型和装箱的对象类型之间差异的简单示例,请考虑method bool SelfSame<T>(T p) { return Object.ReferenceEquals((Object)p,(Object)p);}。对应于值类型的装箱对象类型可以ReferenceEquals通过保留身份的上播满足参数类型。但是,存储位置类型需要非身份保留转换。如果将a强制转换为TU原始名称以外的其他内容的引用T,那么对我而言,a T并不是真正的a U
2014年

5

Equals()System.Object类的方法
语法:公共虚拟布尔Equals()
建议如果我们要比较两个对象的状态,则应使用Equals()方法

如上所述,答案==运算符比较的值是相同的。

请不要与ReferenceEqual混淆

Reference Equals()
语法:public static bool ReferenceEquals()
确定指定对象实例是否属于同一实例。


8
这根本无法回答问题。
SLaks 2014年

我用实例解释了这些问题,这是以上问题的基础。
Sugat Mankar

4

您需要意识到的是,这样做==总是会最终调用一个方法。问题是,是否打电话来==Equals最终打电话/做同样的事情。

对于引用类型,==将始终首先检查引用是否相同(Object.ReferenceEquals)。Equals另一方面,它可以被覆盖,并可以检查某些值是否相等。

编辑:回答svick并添加SLaks注释,这是一些IL代码

int i1 = 0x22; // ldc.i4.s ie pushes an int32 on the stack
int i2 = 0x33; // ldc.i4.s 
short s1 = 0x11; // ldc.i4.s (same as for int32)
short s2 = 0x22; // ldc.i4.s 

s1 == i1 // ceq
i1 == s1 // ceq
i1 == i2 // ceq
s1 == s2 // ceq
// no difference between int and short for those 4 cases,
// anyway the shorts are pushed as integers.

i1.Equals(i2) // calls System.Int32.Equals
s1.Equals(s2) // calls System.Int16.Equals
i1.Equals(s1) // calls System.Int32.Equals: s1 is considered as an integer
// - again it was pushed as such on the stack)
s1.Equals(i1) // boxes the int32 then calls System.Int16.Equals
// - int16 has 2 Equals methods: one for in16 and one for Object.
// Casting an int32 into an int16 is not safe, so the Object overload
// must be used instead.

那么int用== 比较两个s 是什么方法呢?提示:没有operator ==方法Int32,但是有一种方法String
svick 2014年

2
这根本不能回答问题。
SLaks 2014年

@SLaks:确实没有回答有关int和short比较的特定问题,您已经回答了。我仍然感到很有趣的一点==是,它不仅可以做魔术,而且最终只会调用一种方法(大多数程序员可能从未实现/重写任何运算符)。也许我可以为您的问题添加评论,而不是添加自己的答案。如果您觉得我说的话很重要,请随时更新。
user276648 2014年

请注意,==原始类型不是重载运算符,而是编译为ceqIL指令的固有语言功能。
SLaks 2014年

3

==在原始中

Console.WriteLine(age == newAge);          // true

在原始比较中,==运算符的行为非常明显,在C#中,有许多==运算符重载可用。

  • 字符串==字符串
  • 整数==整数
  • uint == uint
  • 长==长
  • 还有很多

因此,在这种情况下,有从没有隐式转换intshort,但是shortint是可能的。因此将newAge转换为int并进行比较,由于两者都具有相同的值,因此返回true。因此,它等效于:

Console.WriteLine(age == (int)newAge);          // true

.Equals()在原始语言中

Console.WriteLine(newAge.Equals(age));         //false

在这里,我们需要查看Equals()方法是什么,我们用一个简短的类型变量调用Equals。因此,存在三种可能性:

  • Equals(object,object)//来自object的静态方法
  • Equals(object)//来自对象的虚方法
  • Equals(short)//实现IEquatable.Equals(short)

这里不是第一种类型,因为参数的数量是不同的,我们只调用一个int类型的参数。如上所述,也消除了第三点,将int隐式转换为short是不可能的。因此,这里Equals(object)称为第二种。该short.Equals(object)是:

bool Equals(object z)
{
  return z is short && (short)z == this;
}

因此,这里测试的条件z is short为假,因为z是一个int,所以它返回假。

这是Eric Lippert的详细文章

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.