我已经阅读了关于何时以及如何覆盖 GetHashCode
的10个不同的问题,但是's still something I don' t得到了 . GetHashCode
的大多数实现都是基于对象字段的哈希码,但是引用的是 GetHashCode
的值在对象的生命周期内永远不会改变 . 如果它所基于的字段是可变的,那该怎么办?另外,如果我希望字典查找等基于引用相等而不是我的重写 Equals
?
为了方便单元测试我的序列化代码,我主要重写 Equals
,我假设序列化和反序列化(在我的情况下为XML)会导致引用相等,所以我想确保至少它的值是正确的 . 在这种情况下,这种不好的做法是否覆盖 Equals
?基本上在大多数执行代码中我想要引用相等而且我总是使用 ==
并且我不会覆盖它 . 我应该创建一个新的方法 ValueEquals
或其他东西,而不是覆盖 Equals
?我曾经认为框架总是使用 ==
而不是 Equals
来比较事情,所以我认为覆盖 Equals
是安全的,因为在我看来它的目的是因为如果你想要有一个与第二个不同的平等定义 ==
运营商 . 从阅读其他几个问题虽然看起来并非如此 .
EDIT:
看来我的意图不清楚,我的意思是99%的时间我想要普通的老参考平等,默认行为,没有惊喜 . 对于非常罕见的情况,我希望值相等,并且我想通过使用 .Equals
而不是 ==
来显式请求值相等 .
当我这样做时,编译器建议我也覆盖 GetHashCode
,并且's how this question came up. It seemed like there'在应用于可变对象时与 GetHashCode
的目标相矛盾,那些是:
-
如果
a.Equals(b)
那么a.GetHashCode()
应== b.GetHashCode()
. -
a.GetHashCode()
的值在a
的生命周期内永远不会改变 .
当一个可变对象时,这些看起来自然是矛盾的,因为如果对象的状态发生变化,我们期望 .Equals()
的值发生变化,这意味着 GetHashCode
应该改变以匹配 .Equals()
中的变化,但 GetHashCode
不应该改变 .
为什么会出现这种矛盾呢?这些建议不适用于可变对象吗?可能是假设,但可能值得一提的是我指的是不是结构的类 .
Resolution:
我从中了解到,在边缘情况下实现所有目标和避免可能的古怪行为的唯一方法是仅基于不可变字段覆盖 Equals
和 GetHashCode
,或实现 IEquatable
. 这种类似似乎削弱了覆盖 Equals
对于引用类型的有用性,因为我在关系数据库中存储它以用它们的主键来识别它们 .
5 回答
如果它所基于的字段是可变的,那该怎么办?
它并不意味着哈希代码会随着对象的变化而改变 . 这是您阅读的文章中列出的所有原因的问题 . 不幸的是,这种问题通常只出现在极端情况下 . 因此开发人员倾向于逃避不良行为 .
如果我确实希望字典查找等基于引用相等而不是我的重写等于?
只要你实现像
IEquatable<T>
这样的接口,这应该不是问题 . 大多数字典实现都会选择一个相等比较器,其方式是使用IEquatable<T>
而不是Object.ReferenceEquals . 即使没有IEquatable<T>
,大多数都默认调用Object.Equals(),然后进入你的实现 .基本上在大多数执行代码中我都希望引用相等而且我总是使用==而我并没有覆盖它 .
如果您希望对象的行为与值相等,则应覆盖==和!=以强制执行所有比较的值相等 . 如果用户实际上想要引用相等,则仍然可以使用Object.ReferenceEquals .
我曾经认为框架总是使用==而不是Equals来比较事物
随着时间的推移,BCL使用的内容有所改变 . 现在大多数使用相等的情况都会使用
IEqualityComparer<T>
实例并将其用于相等 . 在没有指定一个的情况下,他们将使用EqualityComparer<T>.Default
来查找一个 . 在最坏的情况下,这将默认调用Object.Equals如果你有一个可变对象,那么就没有't much point in overriding the GetHashCode method, as you can't真正使用它 . 例如,它被
Dictionary
和HashSet
集合用于将每个项目放入存储桶中 . 如果更改对象's used as a key in the collection, the hash code no longer matches the bucket that the object is in, so the collection doesn' t正常工作,您可能再也找不到该对象 .如果您希望查找不使用类的
GetHashCode
或Equals
方法,则可以在创建Dictionary
时始终提供自己的IEqualityComparer
实现 .Equals
方法适用于 Value 平等,所以以这种方式实施它并没有错 .哇,这实际上是几个问题:-) . 一个接一个地说:
这个常见建议适用于您希望将对象用作HashTable /字典等中的键的情况 . HashTable通常要求哈希不要更改,因为它们使用它来决定如何存储和检索密钥 . 如果散列更改,HashTable可能不再找到您的对象 .
引用Java的Map接口的文档:
注意:如果将可变对象用作映射键,则必须非常小心 . 如果在对象是 Map 中的键的同时以影响等于比较的方式更改对象的值,则不指定映射的行为 .
一般来说,使用任何类型的可变对象作为哈希表中的键是一个坏主意:它已被添加到哈希表中 . 哈希表是应该通过旧密钥,还是通过新密钥,还是通过两者来返回存储的对象?
所以真正的建议是:只使用不可变对象作为键,并确保它们的哈希码永远不会改变(如果对象是不可变的,通常是自动的) .
好吧,找一个像那样工作的字典实现 . 但标准库字典使用哈希码和等号,并且没有办法改变它 .
不,我觉得完全可以接受 . 但是,您不应该将这些对象用作字典/散列表中的键,因为它们是可变的 . 往上看 .
这里的基本主题是如何最好地唯一标识对象 . 您提到序列化/反序列化很重要,因为在该过程中会丢失参照完整性 .
简短的回答是,对象应该由可用于执行此操作的最小不可变字段集唯一标识 . 这些是覆盖GetHashCode和Equals时应使用的字段 .
对于测试,定义所需的任何断言是完全合理的,通常这些断言不是在类型本身上定义,而是在测试套件中定义为实用方法 . 也许是TestSuite.AssertEquals(MyClass,MyClass)?
请注意,GetHashCode和Equals应该一起工作 . 如果两个对象相等,则GetHashCode应返回相同的值 . 当且仅当两个对象具有相同的哈希码时,Equals才会返回true . (请注意,两个对象可能不相等但可能返回相同的哈希码) . 有很多网页可以直接解决这个话题,只是google go away .
我不知道C#,它是一个相对的noob,但在Java中,如果你重写equals(),你还需要覆盖hashCode()来维护它们之间的 Contract (反之亦然)......和java也有同样的捕获22;基本上强迫你使用不可变字段...但这只是用作哈希键的类的一个问题,而Java有所有基于哈希的集合的替代实现......这可能不是那么快,但它们有效地做允许你使用一个可变对象作为一个键...它只是(通常)皱起眉头作为一个“糟糕的设计” .
而且我觉得有必要指出这个根本问题是永恒的......自亚当还是一个小伙子以来,它一直存在 .
我已经研究了比我更老的fortran代码(我36岁),当用户名被更改时就会中断(例如女孩结婚或离婚时;-) ...因此是工程,采用的解决方案是:GetHashCode“方法”记住先前计算的hashCode,重新计算hashCode(即虚拟的isDirty标记),如果keyfields已更改,则返回null . 这会导致缓存删除“脏”用户(通过调用另一个GetPreviousHashCode),然后缓存返回null,导致用户从数据库重新读取 . 一个有趣且有 Value 的黑客;即使我自己这么说;-)
我将权衡O(1)访问的可变性(仅在极端情况下是可取的)(在所有情况下都是可取的) . 欢迎来到工程;知情妥协的土地 .
干杯 . 基思 .