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