在C#中,对于基本类型,"=="和"Equals()"有什么区别?

182

请考虑以下代码:

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

无论是 int 还是 short,它们都是原始类型,但使用 == 进行比较返回 true,而使用 Equals 进行比较返回 false。

为什么呢?


9
@OrangeDog,请先思考问题,再投票关闭。 - user1968030
4
这里缺少一个显而易见的反向尝试:Console.WriteLine(age.Equals(newAge)); - ANeves
3
复制品并没有解释这种行为;它只是关于 Equals() 一般的内容。 - SLaks
38
我几天前在Coverity博客上回答了这个问题。http://blog.coverity.com/2014/01/13/inconsistent-equality/ - Eric Lippert
5
@CodesInChaos说:规范实际上两次使用了“primitive types”这个术语却从未定义它的含义;暗示是primitive types是内置的值类型,但这从未被明确说明。我已建议Mads将该术语从规范中删除,因为它似乎比帮助更多地造成了混淆。 - Eric Lippert
显示剩余16条评论
9个回答

268

简短回答:

平等是复杂的。

详细回答:

基元类型重写了基本的object.Equals(object)方法,并且如果装箱后的object与相同的类型和值,则返回true。(请注意,对于可空类型也适用;非null可空类型始终装箱为基础类型的实例。)

由于newAge是一个short,因此它的Equals(object)方法仅在传递具有相同值的装箱short时返回true。您正在传递一个装箱的int,因此它返回false。

相比之下,==运算符被定义为接受两个int(或shortlong)。
当您使用一个int和一个short调用它时,编译器将隐式地将short转换为int,并按值比较结果为int的两个数。

使其正常工作的其他方法

基元类型还有自己的Equals()方法,接受相同的类型。
如果您编写age.Equals(newAge),编译器将选择int.Equals(int)作为最佳重载,并隐式地将short转换为int。然后它将返回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;
    }

更多阅读:

请参考Eric Lippert的文章。


3
如果我们将 long 等同于 int,那么 int 会被隐式转换为 long,对吗? - Selman Genç
1
而且,是的,我写了所有这些内容,而没有真正尝试过它。 - SLaks
2
@SLaks 是的,但是你回答中的措辞“传递的值”可以有两种解释(作为开发人员传递的值,或CLR在拆箱后实际传递的值)。我猜普通用户如果不知道答案,会将其解释为前者。 - JaredPar
2
@Rachel:除了这个不是真的;默认 == 运算符通过引用比较引用类型。对于值类型和重载 == 的类型,它不会这样做。 - SLaks
1
@Thraka:错了。int不能转换为short。相反,它调用Equals(object),在调用点隐式装箱。请仔细阅读我的答案。 - SLaks
显示剩余11条评论

55

因为没有接受 intshort.Equals 这个重载方法。因此,这被称为:

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

obj不是一个short类型,因此是假的。


12

12
当你将int传递给short的Equals时,你传递了object

enter image description here 因此,这个伪代码运行:

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

10

== 用于检查相等条件,它可以被视为运算符(布尔运算符),只是比较两个东西,这里数据类型并不重要,因为会进行类型转换。而Equals也用于检查相等条件,但在这种情况下,数据类型应该相同。N Equals 是一个方法而不是运算符。

以下是从您提供的示例中摘取的一个小示例,这将简要解释差异。

int x=1;
short y=1;
x==y;//true
y.Equals(x);//false
在上面的例子中,X和Y具有相同的值,即1,当我们使用==时,编译器将short类型转换为int类型并给出结果,因此它将返回true。而当我们使用Equals时,进行比较,但编译器不执行类型转换,因此会返回false。请各位指正。

6

Equals()System.Object类的一个方法
语法:Public virtual bool Equals()
建议如果我们想要比较两个对象的状态,则应使用Equals()方法

如上所述,==操作符比较的值是否相同。

请不要与ReferenceEqual混淆

Reference Equals()
语法:public static bool ReferenceEquals()
它确定指定的对象实例是否是相同的实例


8
这完全没有回答问题。 - SLaks
SLaks并没有用例子解释这个问题的基础知识。 - Sugat Mankar

6
在许多情况下,如果方法或运算符的参数不是所需类型,则C#编译器将尝试执行隐式类型转换。如果编译器可以通过添加隐式转换使所有参数满足其运算符和方法,它将无怨言地这样做,即使在某些情况下(特别是在相等性测试中!)结果可能令人惊讶。
此外,每种值类型(例如int或short)实际上都描述了一种值和一种对象的类型(*)。存在隐式转换以将值转换为其他类型的值,并将任何类型的值转换为其对应的对象类型,但是不同类型的对象不能彼此隐式转换。
如果使用“==”运算符比较short和int,则short将被隐式转换为int。如果其数值等于int的数值,则转换后的int将等于与之比较的int。但是,如果尝试使用Equals方法将short与int进行比较,则唯一能够满足Equals方法重载的隐式转换将是转换为与int相应的对象类型。当要求short是否与传入的对象匹配时,它会观察到该对象是一个int而不是short,因此得出结论它不可能相等。
一般来说,尽管编译器不会抱怨,但应避免比较不同类型的事物;如果想知道将事物转换为公共形式是否会得到相同的结果,则应显式执行此类转换。例如,请考虑:
int i = 16777217;
float f = 16777216.0f;

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

有三种比较int和float的方法。人们可能想知道:
1. 最接近的float值与int是否匹配? 2. float的整数部分是否与int匹配? 3. int和float是否代表相同的数值?
如果直接比较int和float,编译后的代码将回答第一个问题;然而,程序员的意图将很不明显。将比较更改为"(float)i == f"将清楚地表明第一种意义,或者"(double)i == (double)f"将使代码回答第三个问题(并清楚表明这是所期望的)。
即使C#规范认为类型为System.Int32的值是System.Int32类型的对象,这种观点也会被认为在要求在其规范视下值和对象居住在不同宇宙的平台上运行的代码时存在矛盾。此外,如果T是引用类型,而x是T,则类型T的引用应该能够引用x。因此,如果类型为Int32的变量v持有一个Object,那么Object类型的引用应该能够持有对v或其内容的引用。实际上,Object类型的引用将能够指向一个包含从v复制的数据的对象,但不能指向v本身或其内容。这表明v及其内容都不是真正的Object。

1
“唯一可以满足Equals方法重载的隐式转换是转换为对应于int的对象类型”,这是错误的。与Java不同,C#没有单独的原始类型和装箱类型。它被装箱到“object”类型,因为那是“Equals()”的另一个重载。 - SLaks
第一和第三个问题是相同的;在转换为“float”时,确切的值已经丢失。将“float”强制转换为“double”不会神奇地创建新的精度。 - SLaks
@SLaks:关于存储位置类型和装箱对象类型之间差异的简单示例,可以考虑以下方法 bool SelfSame<T>(T p) { return Object.ReferenceEquals((Object)p,(Object)p);}。值类型对应的装箱对象类型可以通过保持标识的向上转换满足ReferenceEquals的参数类型;然而,存储位置类型需要进行非标识保持的转换。如果将T强制转换为U会产生指向原始T以外的引用,则这表明T实际上不是U - supercat
@supercat 能否实际使用"boxed int"类型作为T?据我所知,C++/CLI使用语法表示"boxed int"为int^,对于那些有一些自定义标记的ValueTypeObject进行表示,如果您实际上可以使用它们,这可能有点奇怪。 - CodesInChaos
@CodesInChaos:当像System.Int32这样的类型用于描述存储位置时,它标识了存储位置的类型。当它用于描述堆对象时,它标识了堆对象的类型。CLR没有提供定义引用类型存储位置的方法,该存储位置仅限于保存对类型为Int32的堆对象的引用。 - supercat
显示剩余2条评论

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 会调用哪个方法?提示:Int32 没有 operator == 方法,但是 String 有一个 - svick
2
这完全没有回答问题。 - SLaks
@SLaks:它确实没有回答有关int和short比较的具体问题,你已经回答了。我仍然觉得解释一下==并不是魔法,它最终只是调用一个方法(大多数程序员可能从未实现/覆盖任何运算符)是很有趣的。也许我本可以在你的问题中添加评论,而不是添加自己的答案。如果你认为我说的有意义,请随意更新你的答案。 - user276648
请注意,原始类型上的“==”不是重载运算符,而是一种内在的语言特性,它编译成“ceq”IL指令。 - SLaks

3

== 原始模式

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

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

  • string == string
  • int == int
  • uint == uint
  • long == long
  • 还有更多

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

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

.Equals() 在原始类型中的应用

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

在这里,我们需要了解什么是Equals()方法,我们使用一个short类型变量来调用Equals。因此有三种可能性:
  • Equals(object, object) // 来自object的静态方法
  • Equals(object) // 来自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类型,因此返回false。

这里有Eric Lippert的详细文章


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