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