我读过大约 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 对于引用类型,据我所知,大多数引用类型通常没有不可变字段,除非它们存储在关系数据库中以用主键标识它们。

有帮助吗?

解决方案

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

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

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

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

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

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

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

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

其他提示

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

如果您希望查找不使用 GetHashCode 或者 Equals 类的方法,您始终可以提供自己的方法 IEqualityComparer 当您创建时要使用的实现 Dictionary.

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

哇,这实际上是几个问题合而为一:-)。于是一前一后:

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

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

引用Java的文档 地图 界面:

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

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

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

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

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

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

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

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

简而言之,对象应该由可用于执行此操作的最小不可变字段集唯一标识。这些是您在重写 GetHashCode 和 Equals 时应使用的字段。

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

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

我不了解 C#,因为它是一个相对菜鸟,但在 Java 中,如果你重写 equals(),你还需要重写 hashCode() 来维护它们之间的契约(反之亦然)...而java也有同样的catch 22;基本上强迫你使用不可变字段......但这仅适用于用作散列键的类,并且 Java 对所有基于散列的集合都有替代实现......这可能没那么快,但它们确实有效地允许您使用可变对象作为键......它只是(通常)被认为是“糟糕的设计”而皱起眉头。

我很想指出这个根本问题是永恒的......从亚当还是个小伙子的时候起,它就已经存在了。

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

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

干杯。基思.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top