让Equals成为一个常规方法的目的是什么?

5
这不是关于如何实现的问题,而是关于这个方法的目的是什么?我的意思是 - 好吧,我明白在搜索时需要它,但为什么它被埋藏在“object”类的一个方法中呢?
故事是这样的 - 我有一些默认情况下(在逻辑上)不可比较的对象。每次你想要比较/搜索它们时,你都必须指定匹配方式。在这种情况下最好的方法是:
1. 没有像Equals这样普遍的方法,问题解决了,没有程序员(我的类的用户)会因忽略搜索时的自定义匹配而陷入困境。
但由于我不能更改C#,所以...
2. 隐藏继承的、不需要的方法以防止调用(编译时),但这也需要对C#进行更改。
3. 覆盖Equals并抛出异常 - 至少程序员在运行时得到通知。
所以我问这个问题是因为我被迫使用丑陋的(c),因为(b)不可能并且缺乏(a)。
简而言之,强制所有对象可比较(Equals)的原因是什么?对我来说,这是一个太过假设的前提。非常感谢您的启示。
3个回答

7
我认为在.NET和Java中,这基本上是个错误。对于每个对象都有监视器以及GetHashCode同样如此。
可以说,在泛型出现之前这种做法有些合理,但是考虑到泛型,覆盖Equals(object)总是感觉非常糟糕。
我之前写过博客讨论了这个问题,你可能会发现我的帖子和评论很有趣。

@supercat:您可以拥有一个单一的通用接口来实现相等测试。MemberwiseClone 有点奇怪,部分原因是它在幕后进行了魔法操作。需要更仔细地考虑这个问题... - Jon Skeet
@Jon Skeet:使用接口需要基类使用虚方法来实现它,并将其定义为“补丁点”,或者派生类重新实现相等接口。如果Animal在虚Equals/GetHashCode方法中定义IEquatable,但Dog重新实现接口(即使不应该),而Beagle覆盖了Animal中定义的Equals方法,则尝试比较两个Beagle实例将使用Dog比较方法。最好只有一个单一的补丁点。请注意... - supercat
@Jon Skeet:……比较苹果和橙子的相等性是完全有意义的(尽管不能比较大小)。它们只是不相等。请注意,如果非密封类型上的相等关系是传递的,则不能是通用的。如果要比较两个水果对象,则苹果的Equals方法必须接受任何水果,检查它是否为苹果,如果是,则进行强制转换,并根据此进行比较。如果Equals方法需要类型和转换,那么泛型就没有太多优势了。至于MemberwiseClone... - supercat
@supercat:这取决于您的相等标准。指定一个相等比较器是有意义的,它可以通过体积比较任何两个对象,例如-在这种情况下,苹果和橙子可能是相等的。如果您不想使用泛型,仍然可以拥有一个非泛型的IEquatable类型,它只涉及对象的术语-但这仍然不意味着它必须是System.Object的一部分。 - Jon Skeet
显示剩余5条评论

2
您忘记了第四个选项:什么也不做,让默认的引用相等性生效。在我看来这没有什么大不了的。即使使用您自定义的匹配选项,您也可以选择一个默认选项(我会选择最严格的选项),并使用它来实现Equals()方法。

不,我没有忘记这个选项。如果程序员需要引用比较,则应明确使用引用比较(ReferenceEquals)。换句话说,说出你的意思,不要走捷径。打 Equals 而不是 ReferenceEquals 并假设它们是等价的是一种不好的方法(在我看来)。 - greenoldman

0
假设有一个动物列表,想要将两个项目进行比较:猫的实例和狗的实例。如果询问猫实例是否与狗实例相同,猫抛出InvalidTypeException异常更合理,还是简单地说“不,它们不相等”更合理?
Equals方法应遵守两个规则:
1. 相等的互惠性:对于任何X和Y,只有当Y.Equals(X)为真时,X.Equals(Y)才为真。 2. Liskov替换原则:如果Q类派生自P,则可以使用Q执行的操作也可以使用P执行。
这些规则意味着,如果Q派生自P,则必须能够让类型为P的对象调用类型为Q的对象上的Equals,这又意味着类型为Q的对象必须能够在类型为P的对象上调用Equals。此外,如果R也派生自P,则必须能够让类型为Q的对象在类型为R的对象上调用Equals(无论R是否与Q相关)。

虽然并非所有对象都必须实现Equals,但对于所有类来说,拥有一个单一的Equals(Object)方法比拥有多个针对不同基本类型的Equals方法更加清晰,因为为了避免奇怪的行为,所有这些方法都必须被重写以具有相同的语义。

编辑/补充

Object.Equals存在的目的是回答以下问题:给定两个对象引用X和Y,是否可以保证对象X承诺外部代码(不使用ReferenceEquals、Reflection等)无法显示X和Y不引用同一对象实例?对于任何对象X,X.Equals(X)必须为true,因为外部代码不可能显示X不是与X相同的实例。此外,如果X.Equals(Y)“合法”为true,则Y.Equals(X)也必须为true;否则,X.Equals(X)(为true)与Y.Equals(X)不匹配的事实将是可证明的差异,这意味着X.Equals(Y)应该为false。

对于浅拷贝类型,如果X和Y引用不同的对象实例,可以通过改变X并观察是否在Y中发生相同的变化来证明这一点(*)。 如果这样的变化可以用来证明X和Y是不同的对象实例,则X.Equals(Y)应返回true。两个包含相同字符的String对象之所以会报告它们彼此相等,不仅仅是因为它们恰好在比较时包含相同的字符,更重要的是,如果将其中一个的所有实例替换为另一个,只有使用ReferenceEquals,Reflection或其他类似技巧的代码才会注意到。
(*) 可能存在两个不同但无法区分的浅拷贝类实例X和Y,它们都持有彼此的引用,使得会改变一个实例的方法也会改变另一个实例。如果外部代码没有办法区分这些实例,那么X.Equals(Y)可能合理地报告为true(反之亦然)。另一方面,我想不出这样一个类会比同时持有对共享可变对象的不可变引用的类更有用的方式。请注意,X.Equals(Y)不要求X和Y是深度不可变的;它仅要求应用于X的任何变化对Y具有相同的效果。

虽然并非所有对象都必须实现Equals,但问题确切地就在于此,而不是关于相等性的属性。对于那些适用于Equals的类,我认为为其创建IEqual接口是没有害处的。我有很多类别,检查它们的相等性是没有意义的,或者在设计时和后来使用时强制引入Equals。换句话说,假设我有一个名为Something的类,它根本不应该被比较,只能被存储。 - greenoldman
@macias:对于我在上面编辑中发布的关于相等性问题无法回答哪种对象类型的问题,该如何解决?一个对象要么承诺它将与另一个对象无法区分,要么就不是。 - supercat
你在哪里得到两个对象除了通过ReferenceEquals和Reflection之外无法区分的这部分内容?你似乎将这个作为公理陈述 - 我不同意这一点。例如,在C#中1.00m.Equals(1m)返回true,但它们是可区分的(只需调用ToString)。 - Jon Skeet
@Jon Skeet:我的意图是展示一个可以指定Object.Equals有用行为的方法,以便将任何对象与任何对象进行比较,而不是暗示所有类实际上都以这种方式实现Object.Equals。如果您想建议Equals/GetHashCode不应该应用于所有对象,但没有任何一致的语义,我会同意您的看法。然而,我会建议的解决方案不是放弃"普适"的Equals/GetHashCode方法,而是不要将它们覆盖以表示其他含义的等价性。 - supercat
@supercat:我想我们将不得不同意各执己见。一般来说,如果某个东西不能一致地应用于所有子类型,我就不希望它成为超类型的一部分。 - Jon Skeet
显示剩余6条评论

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