在没有不可变字段的类中重写Object.GetHashCode()时要返回什么?

好吧,在你因为互联网上发布了数百个类似的声音问题而疯狂之前,我可以向你保证,我刚刚花了最后几个小时阅读所有这些问题并且没有找到我的问题的答案。

背景:

基本上,我的一个大型应用程序遇到了ListBox.SelectedItem属性上的某些Binding将停止工作或者在对当前所选项目进行编辑后程序崩溃的情况。 我最初问过“已经添加了相同密钥的项目”这里从代码问题中选择ListBoxItem的例外 ,但没有得到答案。

直到本周,我才有时间解决这个问题。 现在简而言之,我找到了问题的原因。 这是因为我的数据类型类已经覆盖了Equals方法,因此也覆盖了GetHashCode方法。

现在对于那些不知道这个问题的人,我发现你只能使用不可变字段/属性来实现GetHashCode方法。 使用Harvey Kwok对Overriding GetHashCode()post的回答来解释这个:

问题是Dictionary和HashSet集合正在使用GetHashCode将每个项目放在存储桶中。 如果基于某些可变字段计算哈希码,并且在将对象放入HashSet或Dictionary后实际更改了字段,则无法再从HashSet或Dictionary中找到该对象。

所以实际问题是因为我在GetHashCode方法中使用了可变属性。 当用户在UI中更改了这些属性值时,对象的关联哈希码值发生了更改,然后在其集合中找不到项。

题:

所以,我的问题是处理我需要在没有不可变字段的类中实现GetHashCode方法的情况的最佳方法是什么? 对不起,让我更具体一点,因为之前已经提出了这个问题。

Overriding GetHashCode()post中的答案表明,在这些情况下,最好只返回一个常量值……有些建议返回值1 ,而其他建议返回素数。 就个人而言,我看不出这些建议之间有什么区别,因为我原本以为只会有一个桶用于其中任何一个。

此外,Eric Lippert博客中关于GetHashCode的指南和规则有一个标题为指南的部分:哈希码的分布必须是“随机的” ,这突出了使用导致使用不足的桶的算法的缺陷。 他警告说,算法会减少使用的桶数,并在桶变得非常大时导致性能问题 。 当然,返回常数属于这一类。

我想到了为我的所有数据类型类(仅在C#中,而不是数据库中)添加一个额外的Guid字段,特别是在GetHashCode方法中使用。 所以我想在这个长篇介绍的最​​后,我的实际问题是哪个实现更好? 总结一下:

摘要:

在没有不可变字段的类中重写Object.GetHashCode()时,最好从GetHashCode方法返回一个常量,还是为每个类创建一个额外的readonly字段,仅用于GetHashCode方法? 如果我应该添加一个新字段,它应该是什么类型,我不应该将它包含在Equals方法中?

虽然我很高兴收到任何人的答复,但我真的希望得到高级开发人员的答案,他们对这个主题有充分的了解。

回到基础。 你读了我的文章; 再读一遍。 与您的情况相关的两条铁定规则是:

  • 如果x等于y,那么x的哈希码必须等于y的哈希码。 等价地:如果x的哈希码不等于y的哈希码,那么x和y必须是不相等的。
  • 当x在哈希表中时,x的哈希码必须保持稳定。

这些是正确性的要求。 如果你不能保证这两件简单的事情,那么你的程序将是不正确的。

你提出两个解决方案。

您的第一个解决方案是始终返回常量。 这符合两个规则的要求,但您将在哈希表中简化为线性搜索。 你也可以使用一个列表。

您建议的另一个解决方案是以某种方式为每个对象生成哈希码并将其存储在对象中。 如果相等的项具有相同的哈希码,则这是完全合法的。 如果您这样做,那么您受到限制,如果哈希码不同,则x等于y 必须为false。 这似乎使价值平等基本上不可能。 如果你想要引用相等性,你首先不会首先重写Equals,这似乎是一个非常糟糕的主意,但是如果equals是一致的,它是合法的

我提出了第三种解决方案,即:永远不要将对象放在哈希表中,因为哈希表首先是错误的数据结构。 哈希表的要点是快速回答问题“这组不可变值中的给定值是什么?” 并且您没有一组不可变值 ,因此不要使用哈希表。 使用正确的工具完成工作。 使用列表,并忍受线性搜索的痛苦。

第四种解决方案是:对用于相等的可变字段进行散列,在每次变异之前从所有散列表中删除该对象,然后将其放回原处。 这符合两个要求:哈希代码同意相等,哈希表中的对象哈希是稳定的,并且您仍然可以快速查找。

我要么创建一个额外的readonly字段,要么抛出NotSupportedException 。 在我看来,另一种选择毫无意义。 让我们看看为什么。

不同(固定)哈希码

提供不同的哈希码很容易,例如:

 class Sample { private static int counter; private readonly int hashCode; public Sample() { this.hashCode = counter++; } public override int GetHashCode() { return this.hashCode; } public override bool Equals(object other) { return object.ReferenceEquals(this, other); } } 

从技术上讲,你必须注意创建太多的对象并在这里溢出counter ,但实际上我认为这对任何人来说都不会成为一个问题。

这种方法的问题是实例永远不会比较平等。 但是,如果您只想将Sample实例用作其他类型的集合的索引,那就完全没问题了。

常量哈希码

如果存在不同实例应该比较的任何情况,那么乍看之下除了返回常量之外别无选择。 但那会让你离开?

在容器内定位实例将始终退化为等效的线性搜索。 因此,通过返回常量,您可以允许用户为您的类创建一个键控容器,但该容器将展示LinkedList的性能特征。 对于熟悉你class级的人来说,这可能是显而易见的,但我个人认为这让人们开始自责。 如果您事先知道Dictionary不会像预期的那样表现,那么为什么让用户创建一个? 在我看来,最好抛出NotSupportedException

但扔是你不能做的!

有些人会不同意上述情况,当这些人比自己聪明时,人们应该注意。 首先, 此代码分析警告指出GetHashCode不应该抛出。 这是值得考虑的事情,但我们不要教条。 有时你必须打破规则是有原因的。

但是,这还不是全部。 在他关于这个主题的博客文章中 ,Eric Lippert说如果你从GetHashCode内部抛出那么

由于性能原因,您的对象不能成为许多内部使用哈希表的LINQ到对象查询的结果。

失去LINQ肯定是一个无赖,但幸运的是,这条路并没有结束。 使用散列表的许多(所有?)LINQ方法都有重载,它们接受在散列时使用的IEqualityComparer 。 所以你实际上可以使用LINQ,但它会不那么方便。

最后,您必须自己权衡选项。 我的观点是,最好使用白名单策略(在需要时提供IEqualityComparer ),只要它在技术上可行,因为这会使代码显式化:如果有人试图天真地使用该类,他们会得到一个有用的exception告诉他们发生了什么,并且在使用它的任何地方都可以看到相等比较器,使得类的非凡行为立即清晰。

如果类真的不包含任何可以计算哈希值的常量,那么我会使用比GUID更简单的东西。 只需使用类中保留的随机数(或包装类)。

一种简单的方法是将hashCode存储在私有成员中,并在第一次使用时生成它。 如果你的实体不经常更改,并且你不会使用两个不同的Equal(你的Equals方法返回true)的对象作为你的字典中的键,那么这应该没问题:

 private int? _hashCode; public override int GetHashCode() { if (!_hashCode.HasValue) _hashCode = Property1.GetHashCode() ^ Property2.GetHashCode() etc... based on whatever you use in your equals method return _hashCode.Value; } 

但是,如果您有对象a和对象b,其中a.Equals(b)== true,并且您使用a作为键(词典[a] = value)在词典中存储条目。
如果a没有改变,那么dictionary [b]将返回值,但是,如果在将条目存储在字典中之后更改a,则字典[b]很可能会失败。 唯一的解决方法是在任何键更改时重新发送字典。