.NET中的==与Object.Equals(object)有何区别?

53

当我还是个相对新手时,我曾经认为这两个东西是语法糖,使用其中一个与另一个仅仅是个人喜好。随着时间的推移,我发现这两个东西并不相同,即使在默认实现中(参见这里这里)。更让人困惑的是,它们可以分别被覆盖/重载以具有完全不同的含义。

这是一件好事吗?它们之间有什么区别?何时/为什么应该使用其中一个而不是另一个?

9个回答

38
string x = "hello";
string y = String.Copy(x);
string z = "hello";

测试xy是否指向同一对象:

(object)x == (object)y  // false
x.ReferenceEquals(y)    // false
x.ReferenceEquals(z)    // true (because x and z are both constants they
                        //       will point to the same location in memory)

测试xy是否具有相同的字符串值:

x == y        // true
x == z        // true
x.Equals(y)   // true
y == "hello"  // true

请注意,这与Java不同。 在Java中,==运算符没有重载,因此Java中常见的错误是:

y == "hello"  // false (y is not the same object as "hello")

在Java中进行字符串比较时,您需要始终使用.equals()

y.equals("hello")  // true

2
对于字符串运算符“==”,它会比较两个字符串的内容。但是对于其他引用类型,情况并非如此。 - Vaysage
1
我想强调Vaysage刚才说的话:仅使用“string”作为例子是误导性的(或者至少是不完整的)。它只展示了字符串的工作原理,但是字符串是一个特殊情况。为了使这个答案更加完整,需要将string与以下内容进行对比:(a)单个字符,(b)字符数组,(c)包含多个字符字段的struct,(d)包含多个字符字段的class。甚至可能需要展示(e)包含struct字段或包含字符数组字段的class。然后进行各种赋值操作,展示何时结果仍然为true - ToolmakerSteve

17

2
“盲目地”做任何事都是不好的实践。如果你知道问题的答案,为什么还要问呢? - aku
2
@aku:如果您总结两个运算符之间的基本区别,这个答案会更方便。直到第四个链接(覆盖指南),微软才开始同时讨论这两个等式--这是回答问题所必需的。 (我几乎没有点击第四个链接,因为它的标题听起来并不令人兴奋,而第三个链接似乎完全无关。) - ToolmakerSteve
8
为什么这个回答被认可了?它是一个糟糕的回答。 - reach4thelasers
需要单独描述每种类型 - 不仅仅是提供链接。 - thecoolmacdude
需要注意的是,如果 xnull (vb.net 中的 Nothing),那么 x.Equals() 将会抛出异常。 - mbomb007
显示剩余9条评论

9
微软表示,类实现者应该使 == 的行为尽可能与 Equals 相似:

确保 Object.Equals 和等式运算符具有完全相同的语义。

引自:http://msdn.microsoft.com/en-us/library/vstudio/7h9bszxx(v=vs.110).aspx
如果您想要确保获得标识比较(比较引用时),则使用 ReferenceEquals
如果类实现者没有重写 ==,则在编译时会在基类中寻找静态方法。如果查找到 Object,则使用 Object.==。对于类而言,这与 ReferenceEquals 是一样的。
如果类文档不确定某个给定类(来自 Microsoft 以外的供应商)是否将 == 实现为 EqualsReferenceEquals(或者它理论上可能与这两个都不同),我有时会避免使用 ==。相反,我会使用较不可读的 Equals(a, b)ReferenceEquals(a, b),具体取决于我想要的含义。
另一方面,ps2goat 指出,如果第一个操作数为 null,则使用 == 可以避免异常(因为 == 是一个静态运算符)。这是使用 == 的一个优点的论据。
删掉关于 == 有争议的评论
更新 最近的微软文档引用来自 .Net 4.7.2,于 2019 年 2 月检索,显示它们仍然打算使这两个行为相似: Object.Equals Method

某些语言(如 C# 和 Visual Basic)支持运算符重载。当类型重载等式运算符时,它必须还要重写 Equals(Object) 方法以提供相同的功能。通常通过使用以下示例中的重载等式运算符编写 Equals(Object) 方法来完成此操作。


注意:请查看其他答案,了解 == 作为静态方法与 Equals 作为实例方法的后果。我并不声称行为是相同的;我观察到微软建议使这两者尽可能相似。

1
我添加了另一个答案,可能会让你改变一点想法(并不是我介意你的推理)。在空对象上调用.Equals()(抛出异常)与使用静态运算符(不需要实例化任何操作数,没有异常,按预期工作)的区别。 - ps2goat
优秀的回答。谢谢。关于你在这个页面其他地方的评论,提到"为什么许多答案链接到那篇文章" - 这是 StackOverflow:不是阅读完整问题并回答它,而是看你有多快找到问题本身中的问题。这就是为什么批评问题的人在页面顶部,而你的回答最终在 .NET 中澄清了这个混乱,却很难得到投票。 :-) - Derf Skren
@AndrewRondeau - 微软的文档意图是==运算符与Equals行为相同; 因此我的回答中引用了这句话。然而,微软并没有强制执行这一点。如果您作为类设计者选择进行所述区分,则没有任何事情会阻止您。您能描述一个您认为有必要使用此区分的具体情况吗?此外,“您无法更改==的结果”。是的,您可以; 为您的类定义一个==运算符重载。通常,人们将==实现为Equals; 实际上,我参考的微软文档建议这样做。 - ToolmakerSteve
@AndrewRondeau - 我已经更新了我的回答,其中包含了当前的报价,显示微软仍然打算使“==”和“Equals”行为相同。 (在可能的情况下,鉴于一个是实例方法,另一个是静态方法。)你有任何其他引用可以显示不同吗? 不一定来自微软,而是来自任何权威来源,关于C#中良好的编程实践? [正如您从我的答案中看到的,我认为微软的设计不太好,所以我很想听备有详细信息支持的替代建议。] - ToolmakerSteve
1
我在 https://dev59.com/D3VD5IYBdhLWcg3wBm5h#54892972 上的回答中提供了完整的示例,说明何时使用==和Equals。 - Andrew Rondeau
显示剩余5条评论

6
我原本打算在已接受的答案下发布这个评论,但我认为在确定要采取哪种路线时应该考虑这一点。
dotnetfiddle: https://dotnetfiddle.net/gESLzO 代码如下:
    Object a = null;
    Object b = new Object();

    // Ex 1
    Console.WriteLine(a == b);
    // Ex 2
    Console.WriteLine(b == a);

    // Ex 3     
    Console.WriteLine(b.Equals(a));
    // Ex 4
    Console.WriteLine(a.Equals(b));

前三个WriteLine示例可以正常工作,但第四个会抛出异常。1和2使用 == ,这是一个静态方法,不需要实例化任何对象。
示例3有效,因为 b 已经实例化。
示例4失败,因为 a 为 null ,因此无法在空对象上调用方法。
因为我尽可能懒惰地编码,所以我使用 == ,特别是在处理任一对象(或两者都可以)为空的情况时。如果我没有这样做,我必须先进行空检查,然后才能调用 .Equals()。

请注意,这在处理字符串时仍然发生。当然,运算符可以被覆盖,但本答案的关键是运算符是静态的,不需要非空实例作为任何操作数。 - ps2goat

1

我对两者的用途的理解是:使用 == 进行概念上的相等比较(在上下文中,这两个参数是否表示相同的意思?),而使用 .Equals 进行具体的相等比较(这两个参数实际上是否是完全相同的对象?)。

编辑:Kevin Sheffield 的链接文章更好地解释了值类型和引用类型的相等性……


不正确。在 .Net 中,ReferenceEquals身份 测试。如果 Equals 总是进行身份测试,那么拥有两者就没有意义了... - ToolmakerSteve
这是相反的。== 用于具体相等性,Equals 用于概念相等性。 - Andrew Rondeau

1
为了回答这个问题,我们必须描述四种对象等价性:
1. 引用相等性,object.ReferenceEquals(a, b):两个变量指向RAM中完全相同的对象。(如果是C语言,两个变量将具有完全相同的指针。) 2. 可交换性,a == b:两个变量引用完全可互换的对象。因此,当a == b时,Func(a,b)和Func(b,a)执行相同的操作。 3. 语义相等性,object.Equals(a, b):在此刻,两个对象的含义相同。 4. 实体相等性,a.Id == b.Id:两个对象引用相同的实体,例如数据库行,但不必具有相同的内容。
作为程序员,在使用已知类型的对象时,您需要了解适合特定代码时刻业务逻辑的等价性类型。
最简单的例子是字符串与StringBuilder类型。String重写了==,而StringBuilder没有:
var aaa1 = "aaa";
var aaa2 = $"{'a'}{'a'}{'a'}";
var bbb = "bbb";

// False because aaa1 and aaa2 are completely different objects with different locations in RAM
Console.WriteLine($"Object.ReferenceEquals(aaa1, aaa2): {Object.ReferenceEquals(aaa1, aaa2)}");

// True because aaa1 and aaa2 are completely interchangable
Console.WriteLine($"aaa1 == aaa2: {aaa1 == aaa2}");             // True
Console.WriteLine($"aaa1.Equals(aaa2): {aaa1.Equals(aaa2)}");   // True
Console.WriteLine($"aaa1 == bbb: {aaa1 == bbb}");               // False
Console.WriteLine($"aaa1.Equals(bbb): {aaa1.Equals(bbb)}");     // False

// Won't compile
// This is why string can override ==, you can not modify a string object once it is allocated
//aaa1[0] = 'd';

// aaaUpdated and aaa1 point to the same exact object in RAM
var aaaUpdated = aaa1;
Console.WriteLine($"Object.ReferenceEquals(aaa1, aaaUpdated): {Object.ReferenceEquals(aaa1, aaaUpdated)}"); // True

// aaaUpdated is a new string, aaa1 is unmodified
aaaUpdated += 'c';
Console.WriteLine($"Object.ReferenceEquals(aaa1, aaaUpdated): {Object.ReferenceEquals(aaa1, aaaUpdated)}"); // False

var aaaBuilder1 = new StringBuilder("aaa");
var aaaBuilder2 = new StringBuilder("aaa");

// False, because both string builders are different objects
Console.WriteLine($"Object.ReferenceEquals(aaaBuider1, aaaBuider2): {Object.ReferenceEquals(aaa1, aaa2)}");

// Even though both string builders have the same contents, they are not interchangable
// Thus, == is false
Console.WriteLine($"aaaBuider1 == aaaBuilder2: {aaaBuilder1 == aaaBuilder2}");

// But, because they both have "aaa" at this exact moment in time, Equals returns true
Console.WriteLine($"aaaBuider1.Equals(aaaBuilder2): {aaaBuilder1.Equals(aaaBuilder2)}");

// Modifying the contents of the string builders changes the strings, and thus
// Equals returns false
aaaBuilder1.Append('e');
aaaBuilder2.Append('f');
Console.WriteLine($"aaaBuider1.Equals(aaaBuilder2): {aaaBuilder1.Equals(aaaBuilder2)}");

为了更详细地了解,我们可以逆向思考,从实体的相等性开始。在实体相等性的情况下,实体的属性可能随时间而变化,但实体的主键永远不会改变。这可以用伪代码来证明:
// Hold the current user object in a variable
var originalUser = database.GetUser(123);

// Update the user’s name
database.UpdateUserName(123, user.Name + "son");

var updatedUser = database.GetUser(123);

Console.WriteLine(originalUser.Id == updatedUser.Id); // True, both objects refer to the same entity
Console.WriteLine(Object.Equals(originalUser, updatedUser); // False, the name property is different

转向语义相等性,示例稍作更改:
var originalUser = new User() { Name = "George" };
var updatedUser = new User() { Name = "George" };

Console.WriteLine(Object.Equals(originalUser, updatedUser); // True, the objects have the same contents
Console.WriteLine(originalUser == updatedUser); // User doesn’t define ==, False

updatedUser.Name = "Paul";

Console.WriteLine(Object.Equals(originalUser, updatedUser); // False, the name property is different

关于可互换性怎么样?(覆盖 ==)这更加复杂。让我们在上面的例子基础上进行一些构建:
var originalUser = new User() { Name = "George" };
var updatedUser = new User() { Name = "George" };
Console.WriteLine(Object.Equals(originalUser, updatedUser); // True, the objects have the same contents

// Does this change updatedUser? We don’t know
DoSomethingWith(updatedUser);

// Are the following equivalent?
// SomeMethod(originalUser, updatedUser);
// SomeMethod(updatedUser, originalUser);

在上面的例子中,DoSomethingWithUser(updatedUser) 可能会改变 updatedUser。因此,我们不能再保证 originalUser 和 updatedUser 对象是“相等”的。这就是为什么 User 没有重写 == 的原因。
一个很好的例子是不可变对象。不可变对象是指其公开可见状态(属性)永远不会改变的对象。整个可见状态必须在对象的构造函数中设置。(因此,所有属性都是只读的。)
var originalImmutableUser = new ImmutableUser(name: "George");
var secondImmutableUser = new ImmutableUser(name: "George");

Console.WriteLine(Object.Equals(originalImmutableUser, secondImmutableUser); // True, the objects have the same contents
Console.WriteLine(originalImmutableUser == secondImmutableUser); // ImmutableUser defines ==, True

// Won’t compile because ImmutableUser has no setters
secondImmutableUser.Name = "Paul";

// But this does compile
var updatedImmutableUser = secondImmutableUser.SetName("Paul"); // Returns a copy of secondImmutableUser with Name changed to Paul.

Console.WriteLine(object.ReferenceEquals(updatedImmutableUser, secondImmutableUser)); // False, because updatedImmutableUser is a different object in a different location in RAM

// These two calls are equivalent because the internal state of an ImmutableUser can never change
DoSomethingWith(originalImmutableUser, secondImmutableUser);
DoSomethingWith(secondImmutableUser, originalImmutableUser);

你是否应该覆盖可变对象的 == 操作符?(即,内部状态可以更改的对象?)可能不需要。您需要构建一个相当复杂的事件系统来维护互换性。
通常,我使用很多使用不可变对象的代码,因此我覆盖 ==,因为它比 object.Equals 更可读。当我使用可变对象时,我不覆盖 == 并依赖于 object.Equals。程序员有责任知道他们正在使用的对象是否可变,因为知道某些东西的状态是否可以更改应影响您设计代码的方式。
== 的默认实现是 object.ReferenceEquals,因为对于可变对象,仅当变量指向 RAM 中完全相同的对象时才保证互换性。即使在给定时间点上对象具有相同的内容(Equals 返回 true),也不能保证对象将继续相等;因此对象不可互换。因此,在使用未覆盖 == 的可变对象时,== 的默认实现有效,因为如果 a == b,则它们是相同的对象,并且 SomeFunc(a, b) 和 SomeFunc(b, a) 完全相同。
此外,如果一个类没有定义相等性(例如,考虑数据库连接、打开的文件句柄等),那么==和Equals的默认实现会回退到引用相等性,因为类型为数据库连接、打开的文件句柄等的两个变量仅在它们是完全相同的数据库连接、打开的文件句柄等实例时才相等。实体相等在业务逻辑中可能是有意义的,需要知道两个不同的数据库连接是否引用同一个数据库,或者两个不同的文件句柄是否引用磁盘上的同一文件。
现在,我想说一下我的看法。在我看来,C# 对这个主题的处理方式很令人困惑。==应该用于语义相等,而不是Equals方法。应该有一个不同的运算符,比如===,用于可互换性,还可以有另一个运算符,====,用于引用相等性。这样,一个新手或编写CRUD应用程序的人只需要理解==,而不需要理解更微妙的可互换性和引用相等性的细节。

0

你可能想使用.Equals,因为有人可能会在以后重载你的类。


是的,我想我本来想说反过来了。 - Ed S.

-1

两种最常用的类型,String和Int32,都实现了operator==()和Equals()作为值相等(而不是引用相等)的操作符。我认为可以将这两个类型视为定义性例子,因此我的结论是它们具有相同的含义。如果Microsoft 声明相反,我认为他们是有意造成混淆。


1
在 .net 中,覆盖相等/不相等运算符的类型是为了强制进行值相等性比较,但 C# 添加了自己的相等/不相等运算符重载来检查对象的引用相等性,这些运算符并不包括值相等性测试。就个人而言,我不喜欢这样的语言设计(vb.net 使用运算符 IsIsNot 来测试引用相等性;当应用于 Framework 类型时,=<> 运算符将测试值相等性,如果它们能编译的话。然而,并没有什么可以阻止任何类型重载这些运算符以完全不同的含义。 - supercat

-1

当我们比较值而不是引用时,运算符==和Equals()是相同的。两者的输出结果相同,如下所示。

示例

    static void Main()
    {
        string x = " hello";
        string y = " hello";
        string z = string.Copy(x);
        if (x == y)
        {
            Console.WriteLine("== Operator");
        }
        if(x.Equals(y))
        {
            Console.WriteLine("Equals() Function Call");
        }
        if (x == z)
        {
            Console.WriteLine("== Operator while coping a string to another.");
        }
        if (x.Equals(y))
        {
            Console.WriteLine("Equals() Function Call while coping a string to another.");
        }
    }

输出:

  == Operator
  Equals() Function Call
  == Operator while coping a string to another.
  Equals() Function Call while coping a string to another.

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