61

我已经阅读了大约 10 个关于何时以及如何覆盖的不同问题,GetHashCode但仍有一些我不太明白的问题。的大多数实现GetHashCode都是基于对象字段的哈希码,但有人指出,GetHashCode在对象的生命周期内,值永远不会改变。如果它所基于的字段是可变的,那它是如何工作的?另外,如果我确实希望字典查找等基于引用相等而不是我的覆盖Equals怎么办?

我主要是Equals为了便于对我的序列化代码进行单元测试而重写,我假设序列化和反序列化(在我的情况下为 XML)会杀死引用相等,所以我想确保它至少通过值相等是正确的。Equals在这种情况下,这是一种不好的做法吗?基本上在大多数执行代码中,我想要引用相等,我总是使用==并且我没有覆盖它。我应该只创建一个新方法ValueEquals或其他东西而不是覆盖Equals吗?我曾经假设框架总是使用==而不是Equals比较事物,所以我认为重写它是安全的,Equals因为在我看来,它的目的是如果你想要一个不同于==操作员。从阅读其他几个问题来看,情况似乎并非如此。

编辑:

似乎我的意图不清楚,我的意思是 99% 的时间我想要简单的旧引用相等,默认行为,没有意外。对于非常罕见的情况,我想要值相等,并且我想通过使用.Equals而不是显式请求值相等==

当我这样做时,编译器建议我也覆盖GetHashCode,这就是这个问题的出现方式。GetHashCode当应用于可变对象时,似乎存在矛盾的目标,这些目标是:

  1. 如果a.Equals(b)那么a.GetHashCode()应该== b.GetHashCode()
  2. 的值在a.GetHashCode()的生命周期内永远不会改变a

当一个可变对象时,这些看起来自然是矛盾的,因为如果对象的状态发生变化,我们期望 的值发生.Equals()变化,这意味着GetHashCode应该改变以匹配 的变化.Equals(),但GetHashCode不应该改变。

为什么会出现这种矛盾?这些建议是否不适用于可变对象?可能是假设的,但可能值得一提的是,我指的是类而不是结构。

解析度:

我将 JaredPar 标记为已接受,但主要用于评论交互。总结一下我从中学到的是,实现所有目标并避免极端情况下可能出现的古怪行为的唯一方法是仅覆盖EqualsGetHashCode基于不可变字段,或实现IEquatable. 这种似乎减少了覆盖Equals引用类型的用处,因为据我所见,大多数引用类型通常没有不可变字段,除非它们存储在关系数据库中以用它们的主键来识别它们。

4

5 回答 5

22

如果它所基于的字段是可变的,那它是如何工作的?

从某种意义上说,哈希码不会随着对象的变化而变化。由于您阅读的文章中列出的所有原因,这是一个问题。不幸的是,这种问题通常只出现在极端情况下。因此,开发人员倾向于逃避不良行为。

另外,如果我确实希望字典查找等基于引用相等而不是我重写的 Equals 怎么办?

只要你实现这样的接口IEquatable<T>应该不是问题。大多数字典实现将选择一个相等比较器,以使用IEquatable<T>Object.ReferenceEquals。即使没有IEquatable<T>,大多数人都会默认调用 Object.Equals() ,然后将其进入您的实现。

基本上在大多数执行代码中,我想要引用相等,我总是使用 == 并且我没有覆盖它。

如果您希望您的对象具有值相等的行为,您应该覆盖 == 和 != 以强制所有比较的值相等。如果用户确实想要引用相等,他们仍然可以使用 Object.ReferenceEquals。

我曾经假设框架总是使用 == 而不是 Equals 来比较事物

BCL 使用的内容随着时间的推移发生了一些变化。现在大多数使用相等的情况将采用一个IEqualityComparer<T>实例并将其用于相等。在未指定一个的情况下,他们将使用EqualityComparer<T>.Default查找一个。在最坏的情况下,这将默认调用 Object.Equals

于 2009-05-17T01:07:07.030 回答
7

如果您有一个可变对象,则重写 GetHashCode 方法没有多大意义,因为您不能真正使用它。例如,DictionaryandHashSet集合使用它来将每个项目放入存储桶中。如果在对象用作集合中的键时更改对象,则哈希码不再匹配对象所在的存储桶,因此集合无法正常工作,您可能再也找不到对象。

如果您希望查找不使用类的GetHashCodeorEquals方法,您可以随时提供自己的IEqualityComparer实现以在创建Dictionary.

Equals方法旨在实现价值平等,因此以这种方式实现它并没有错。

于 2009-05-17T01:21:55.433 回答
5

哇,这实际上是一个问题:-)。于是一个接一个:

有人引用 GetHashCode 的值在对象的生命周期内永远不应该改变。如果它所基于的字段是可变的,那它是如何工作的?

此常见建议适用于您希望将对象用作哈希表/字典等中的键的情况。HashTables 通常要求哈希不要改变,因为他们使用它来决定如何存储和检索密钥。如果散列发生变化,HashTable 将可能不再找到您的对象。

引用 Java 的Map接口的文档:

注意:如果将可变对象用作映射键,则必须非常小心。如果对象的值以影响等于比较的方式更改,而对象是映射中的键,则不指定映射的行为。

一般来说,使用任何类型的可变对象作为哈希表中的键是一个坏主意:如果键在添加到哈希表后发生更改,甚至不清楚会发生什么。哈希表应该通过旧键,还是通过新键,或者两者都返回存储的对象?

所以真正的建议是:只使用不可变对象作为键,并确保它们的哈希码也永远不会改变(如果对象是不可变的,这通常是自动的)。

另外,如果我确实希望字典查找等基于引用相等而不是我重写的 Equals 怎么办?

好吧,找到一个像这样工作的字典实现。但是标准库字典使用 hashcode&Equals,没有办法改变它。

我主要重写 Equals 以便于对我的序列化代码进行单元测试,我假设序列化和反序列化(在我的情况下为 XML)会杀死引用相等,所以我想确保它至少通过值相等是正确的。在这种情况下覆盖 Equals 是不好的做法吗?

不,我觉得这完全可以接受。但是,您不应将此类对象用作字典/哈希表中的键,因为它们是可变的。往上看。

于 2009-05-17T01:13:39.463 回答
2

我不知道 C#,它是一个相对的菜鸟,但在 Java 中,如果你覆盖 equals(),你还需要覆盖 hashCode() 以维护它们之间的合同(反之亦然)......而 java也有相同的捕获 22;基本上强迫你使用不可变的字段......但这仅适用于用作哈希键的类,并且Java对所有基于哈希的集合都有替代实现......这可能没有那么快,但它们确实有效允许您使用可变对象作为键......它只是(通常)被视为“糟糕的设计”而皱眉。

我很想指出这个基本问题是永恒的……自从亚当还是个小伙子以来,它就一直存在。

我已经研究过比我(我 36 岁)更老的 fortran 代码,它在更改用户名时会中断(例如当一个女孩结婚或离婚时;-) ...这就是工程,采用的解决方案是:GetHashCode“方法”记住之前计算的hashCode,重新计算hashCode(即一个虚拟的isDirty标记),如果keyfields已经改变它返回null。这会导致缓存删除“脏”用户(通过调用另一个 GetPreviousHashCode),然后缓存返回 null,导致用户重新从数据库中读取。一个有趣且有价值的 hack;即使我自己这么说;-)

我将为 O(1) 访问(在所有情况下都需要)权衡可变性(仅在极端情况下需要)。欢迎来到工程;知情妥协之地。

干杯。基思。

于 2009-05-17T06:57:17.377 回答
1

这里的基本主题是如何最好地唯一标识对象。您提到序列化/反序列化很重要,因为在该过程中会丢失参照完整性。

简短的回答是,对象应该由可用于这样做的最小的不可变字段集唯一标识。这些是您在覆盖 GetHashCode 和 Equals 时应该使用的字段。

对于测试,定义您需要的任何断言是完全合理的,通常这些断言不是在类型本身上定义的,而是作为测试套件中的实用方法。也许是 TestSuite.AssertEquals(MyClass, MyClass) ?

请注意,GetHashCode 和 Equals 应该一起工作。如果两个对象相等,GetHashCode 应该返回相同的值。当且仅当两个对象具有相同的哈希码时,Equals 应该返回 true。(请注意,两个对象可能不相等但可能返回相同的哈希码)。有很多网页可以正面解决这个话题,只需谷歌即可。

于 2009-05-17T01:35:10.300 回答