覆盖可变对象的GetHashCode?

我已经阅读了有关何时以及如何覆盖Equals的10个不同的问题,但是仍然有一些我不太了解的问题。 GetHashCode的大多数实现都是基于对象字段的哈希码,但有人指出IEquatable的值在对象的整个生命周期中都不应改变。 如果它基于的字段是可变的,那该如何工作? 如果我希望字典查找等基于引用相等性而不是我被覆盖的Equals,该怎么办?

我主要是重写Equals,以方便对序列化代码进行单元测试,而我假设序列化和反序列化(在我的情况下为XML)会杀死引用相等性,因此我想确保至少通过值相等性是正确的。 在这种情况下,是否要重写GetHashCode,这是一种不好的做法? 基本上,在大多数执行代码中,我都希望引用相等,并且我始终使用IEquatable,而我并没有覆盖它。 我应该创建一个新方法Equals还是替代Equals? 我曾经假设该框架始终使用==而不是Equals进行比较,所以我认为覆盖Equals是安全的,因为在我看来,它的目的是如果您要拥有一个不同于的第二种平等定义。 ==操作员。 通过阅读其他几个问题,似乎并非如此。

编辑:

似乎我的意图不明确,我的意思是说我有99%的时间想要简单的旧引用相等,默认行为,没有惊喜。 在极少数情况下,我想拥有值相等性,并且我想通过使用Equals而不是GetHashCode显式地请求值相等性。

执行此操作时,编译器建议我也覆盖Equals,这就是出现此问题的方式。 应用于可变对象时,似乎GetHashCode的目标相互矛盾,这些目标是:

  1. 如果Equals,则GetHashCode应该IEquatable
  2. Equals的值在GetHashCode的生命周期内不应更改。

当一个可变对象时,这些看起来自然矛盾,因为如果对象的状态发生变化,我们期望Equals的值发生变化,这意味着GetHashCode应该进行更改以匹配IEquatable中的更改,而Equals则不应更改。

为什么似乎存在这种矛盾? 这些建议是否不打算适用于可变对象? 可能是假定的,但也许值得一提,我指的是类而不是结构。

解析度:

我将JaredPar标记为接受,但主要用于评论交互。 总结一下,我得出的结论是,实现所有目标并避免在极端情况下可能出现的古怪行为的唯一方法是仅基于不可变字段覆盖EqualsGetHashCode,或实施IEquatable。这种情况似乎正在减少 从参考文献中可以看出,重写Equals对引用类型的用处,正如我所看到的那样,除非将引用类型存储在关系数据库中以使用主键进行标识,否则通常没有不可变字段。

Davy8 asked 2019-11-07T11:01:11Z
5个解决方案
22 votes

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

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

另外,如果我希望字典查找等基于引用相等而不是基于覆盖的相等怎么办?

只要您实现像IEqualityComparer<T>这样的接口,这都不是问题。 大多数词典实现将选择对等比较器,该比较器将在Object.ReferenceEquals上使用EqualityComparer<T>.Default。 即使没有IEquatable<T>,大多数代码也会默认调用Object.Equals(),然后它将进入您的实现中。

基本上,在大多数执行代码中,我都希望引用相等,并且我始终使用==,而我并没有覆盖它。

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

我曾经假设框架始终使用==而不是等于来比较事物

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

JaredPar answered 2019-11-07T11:02:17Z
6 votes

如果您有一个可变的对象,则覆盖GetHashCode方法没有什么意义,因为您无法真正使用它。 例如,EqualsEquals集合使用它来将每个项目放置在存储桶中。 如果在将对象用作集合中的键的同时更改对象,则哈希码将不再与该对象所在的存储桶匹配,因此该集合将无法正常工作,并且您可能再也找不到该对象。

如果您希望查找不使用该类的EqualsEquals的方法,则可以始终提供自己的IEqualityComparer实现,以在创建Dictionary时使用。

Equals方法旨在实现值相等,因此以这种方式实现它不是错误的。

Guffa answered 2019-11-07T11:03:02Z
4 votes

哇,实际上实际上是几个问题:-)。 所以一个接一个:

有人指出,GetHashCode的值在对象的整个生命周期中都不应改变。 如果它基于的字段是可变的,那该如何工作?

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

引用Java的Map接口的文档:

注意:如果将可变对象用作地图键,则必须格外小心。 如果在对象是映射中的键的情况下以影响等值比较的方式更改对象的值,则不会指定映射的行为。

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

因此,真正的建议是:仅将不可变对象用作键,并确保其哈希码也不会改变(如果对象是不可变的,通常这是自动的)。

另外,如果我希望字典查找等基于引用相等而不是基于覆盖的相等怎么办?

好吧,找到一个像这样工作的字典实现。 但是标准库字典使用哈希码和Equals,并且无法更改它。

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

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

sleske answered 2019-11-07T11:04:43Z
2 votes

我不知道C#是相对于它的菜鸟,但是在Java中,如果您重写equals(),还需要重写hashCode()来保持它们之间的契约(反之亦然)...而且java 也有相同的渔获22; 基本上迫使您使用不可变字段...但这仅对于用作哈希键的类是一个问题,并且Java为所有基于哈希的集合提供了替代实现...虽然速度可能不快,但它们确实有效 允许您将可变对象用作键...(通常)只是因为“可怜的设计”而皱眉。

我渴望指出这一基本问题是永恒的……自从亚当小伙子以来就一直存在。

我从事过比我年龄大(36岁)的fortran代码,该代码在更改用户名时会中断(例如当女孩结婚或离婚时;-)。因此,在工程方面,采用的解决方案是 :GetHashCode“方法”会记住先前计算的hashCode,重新计算hashCode(即虚拟isDirty标记),如果关键字段已更改,则返回null。 这将导致缓存删除“脏”用户(通过调用另一个GetPreviousHashCode),然后缓存返回null,从而导致该用户从数据库中重新读取。 一个有趣且有价值的hack; 即使我自己也这么说;-)

我将权衡针对O(1)访问(在所有情况下都需要)的可变性(仅在特殊情况下才需要)。 欢迎来到工程; 知情妥协的土地。

干杯。 基思

corlettk answered 2019-11-07T11:05:38Z
1 votes

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

简短的答案是:对象应该由可用于实现此目的的最小不可变字段集唯一标识。 这些是重写GetHashCode和Equals时应使用的字段。

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

请注意,GetHashCode和Equals应该一起工作。 如果两个对象相等,则GetHashCode应该返回相同的值。 当且仅当两个对象具有相同的哈希码时,等于才应返回true。 (请注意,两个对象可能不相等,但可能返回相同的哈希码)。 有很多网页可以直接解决这个主题,而Google不在。

Joe answered 2019-11-07T11:06:27Z
translate from https://stackoverflow.com:/questions/873654/overriding-gethashcode-for-mutable-objects