【发布时间】:2010-09-11 20:59:55
【问题描述】:
所以,当我还是现在的新手时,我曾经认为这两个东西是彼此的语法糖,即使用一个而不是另一个只是个人喜好。随着时间的推移,我发现这两者并不是一回事,即使在默认实现中也是如此(参见this 和this)。为了进一步混淆这个问题,每个都可以单独覆盖/重载以具有完全不同的含义。
这是一件好事吗,有什么区别,什么时候/为什么应该使用一个而不是另一个?
【问题讨论】:
标签: .net
所以,当我还是现在的新手时,我曾经认为这两个东西是彼此的语法糖,即使用一个而不是另一个只是个人喜好。随着时间的推移,我发现这两者并不是一回事,即使在默认实现中也是如此(参见this 和this)。为了进一步混淆这个问题,每个都可以单独覆盖/重载以具有完全不同的含义。
这是一件好事吗,有什么区别,什么时候/为什么应该使用一个而不是另一个?
【问题讨论】:
标签: .net
要回答这个问题,我们必须描述四种对象等价:
Reference Equality, object.ReferenceEquals(a, b):这两个变量指向 RAM 中的同一个对象。 (如果这是 C,两个变量将具有相同的精确指针。)
Interchangeability, a == b:这两个变量指的是完全可以互换的对象。因此,当 a == b 时,Func(a,b) 和 Func(b,a) 做同样的事情。
语义平等,object.Equals(a, b):在这个确切的时刻,这两个对象的含义相同。
实体相等,a.Id == b.Id:两个对象引用同一个实体,例如数据库行,但不必具有相同的内容.
作为程序员,在处理已知类型的对象时,您需要了解在您所处的特定代码时刻适合您的业务逻辑的等价类型。
最简单的例子是字符串与 StringBuilder 类型。字符串覆盖 ==,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 对象是“相等的”。这就是用户不覆盖 == 的原因。
何时覆盖 == 的一个很好的例子是使用不可变对象。不可变对象是公开可见状态(属性)永不改变的对象。必须在对象的构造函数中设置整个可见状态。 (因此,所有属性都是只读的。)
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 应用程序的人只需要了解 ==,而不需要了解可互换性和引用相等性的更细微的细节。
【讨论】:
Microsoft 表示,类实现者应该使 == 的行为尽可能与 Equals 相似:
务必确保 Object.Equals 和相等运算符具有完全相同的语义
来自http://msdn.microsoft.com/en-us/library/vstudio/7h9bszxx(v=vs.110).aspx
如果您想确定您正在获得 IDENTITY 比较(比较参考时),请改用 ReferenceEquals。
如果类实现者没有覆盖==,则在编译时在基类中查找静态方法。如果此搜索到达Object,则使用Object.==。对于类,这与ReferenceEquals 相同。
如果类文档不确定给定类(可能来自 Microsoft 以外的供应商)是否将 == 实现为 Equals 或 ReferenceEquals(或者理论上可能不同于这两者),
我有时会避开==。相反,我使用可读性较差的Equals(a, b) 或ReferenceEquals(a, b),具体取决于我想要的含义。
OTOH,ps2goat 提出了一个很好的观点,即如果第一个操作数为空,使用 == 可以避免异常(因为 == 是静态运算符)。这是支持使用== 的论据。
删除了关于==的有争议的评论
更新 2019 年 2 月检索到的来自 .Net 4.7.2 的最新 Microsoft 文档引用表明,他们仍然希望两者表现相似:
某些语言(例如 C# 和 Visual Basic)支持运算符重载。当类型重载相等运算符时,它还必须重写 Equals(Object) 方法以提供相同的功能。这通常通过根据重载的相等运算符编写 Equals(Object) 方法来完成,如下例所示。
注意:请参阅其他答案,了解 == 是静态方法与 Equals 是实例方法的后果。我并不是说行为是相同的;我观察到 Microsoft 建议使两者尽可能相似。
【讨论】:
.Equals()(抛出异常)与使用不需要实例化任一操作数的静态运算符(无异常,按预期工作)。
== 运算符的行为与 Equals 相同;因此在我的回答中引用。 OTOH,Microsoft 不强制执行此操作。如果您作为班级设计师选择做出您陈述的区别,那么没有什么可以阻止您。你能描述一个你认为这是可取的具体情况吗?另外,关于“你不能改变==的结果”。是的你可以;为您的类定义运算符== 的重载。通常将== 实现为Equals;确实是我参考的 Microsoft 文档 recommends 这样做。
我打算将此作为对已接受答案的评论发布,但我认为在确定采取哪条路线时值得考虑这一点。
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));
前 3 个 WriteLine 示例可以工作,但第四个会引发异常。 1 和 2 使用==,这是一个静态方法,不需要实例化任何一个对象。
示例 3 有效,因为 b 已实例化。
示例 4 失败,因为 a 是 null,因此无法对空对象调用方法。
因为我尝试尽可能懒惰地编写代码,所以我使用==,尤其是在处理任何一个对象(或两者)都可以为空的情况时。如果我没有,我必须先做一个空检查,然后才能调用.Equals()。
【讨论】:
当我们比较值而不是引用时,Operator == 和 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.
【讨论】:
string x = "hello";
string y = String.Copy(x);
string z = "hello";
测试x是否与y指向同一个对象:
(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)
测试x是否与y具有相同的字符串值:
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
【讨论】:
string 与:(a) 单个字符,(b) 字符数组,(c) 包含多个字符字段的struct,(d) 包含多个字符字段的class 进行对比。字符字段。甚至可能需要显示 (e) 包含 struct 字段或包含 character array 字段的 class。然后做各种赋值,当结果还是true时显示。
两种最常用的类型,String 和 Int32,将 operator==() 和 Equals() 实现为值相等(而不是引用相等)。我认为可以考虑这两个定义示例,因此我的结论是两者具有相同的含义。如果是微软states otherwise,我认为他们是故意造成混乱的。
【讨论】:
Is 和 IsNot 来测试引用相等性;当应用于框架类型时,= 和 <> 将测试值相等性,如果它们完全编译的话。然而,没有什么可以阻止任何类型重载这些运算符以表示完全不同的东西。
您可能希望使用 .Equals,因为稍后有人可能会出现并为您的课程重载它们。
【讨论】:
MSDN 对这两件事都有清晰而可靠的描述。
Guidelines for Overriding Equals() and Operator ==
这是一件好事吗? 差异,以及何时/为什么应该 使用一个而不是另一个?
它怎么可能是“好”或“坏”的东西?一个 - 方法,另一个 - 运算符。如果引用相等性不够,则重载它们,否则保持原样。对于原始类型,它们只是开箱即用。
【讨论】:
我对两者用法的理解是这样的:使用 == 表示概念上的相等(在上下文中,这两个参数是否表示相同的意思?),以及 .Equals 表示具体相等(这两个参数实际上是否准确同一个对象?)。
编辑:Kevin Sheffield 的链接文章在解释价值与参考平等方面做得更好……
【讨论】:
ReferenceEquals 是 .Net 中的 identity 测试。如果Equals 总是进行身份测试,那么两者都没有意义......