【问题标题】:What to return when overriding Object.GetHashCode() in classes with no immutable fields?在没有不可变字段的类中覆盖 Object.GetHashCode() 时返回什么?
【发布时间】:2013-10-31 15:04:28
【问题描述】:

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

背景:

基本上,我的一个大型应用程序遇到了这样一种情况:ListBox.SelectedItem 属性上的某些Bindings 将停止工作,或者在对当前选定的项目进行编辑后程序会崩溃。我最初在这里问了'An item with the same key has already been added' Exception on selecting a ListBoxItem from code 的问题,但没有得到答案。

直到本周我才有时间解决这个问题,当时我有几天的时间来解决这个问题。现在长话短说,我找到了问题的原因。这是因为我的数据类型类覆盖了 Equals 方法,因此也覆盖了 GetHashCode 方法。

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

问题在于 Dictionary 和 HashSet 集合使用 GetHashCode 将每个项目放入存储桶中。如果hashcode是根据一些可变字段计算出来的,而对象放入HashSet或Dictionary后字段确实发生了变化,则无法再从HashSet或Dictionary中找到该对象。

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

问题:

那么,我的问题是处理需要在没有不可变字段的类中实现 GetHashCode 方法的情况的最佳方法是什么?抱歉,让我说得更具体一些,因为 这个问题 以前被问过。

Overriding GetHashCode() 帖子中的答案表明,在这些情况下,最好简单地返回一个常量值......有些人建议返回值 1,而另一些人建议返回一个素数。就个人而言,我看不出这些建议之间有什么区别,因为我原以为它们中的任何一个都只会使用一个存储桶。

此外,Eric Lippert 博客中的 Guidelines and rules for GetHashCode 文章有一个标题为指南:哈希码的分布必须是“随机的”的部分,它强调了使用导致不够的算法的缺陷正在使用的桶。他警告说,算法会减少使用的存储桶数量,并在存储桶变得非常大时导致性能问题。当然,返回一个常量就属于这一类。

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

总结:

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

虽然我很高兴收到任何人的回答,但我真的希望收到对此主题有深入了解的高级开发人员的回答。

【问题讨论】:

  • 如果你手头有一本 Effective C# 的副本,如果你还没有读过的话,第 7 条就是关于这个的。
  • 如果使用仅限于一个位置,一个简单的解决方法是将类型包装在另一个提供唯一不可变值的类中,可能是Guid,并使用它来获取哈希码。我个人会尽量不要将Guid 添加到仅用于字典的类型中。或者,您可以使用身份映射之类的东西来为基于分离的不可变 ID 的对象设置键(同样可能是 Guid,因此效果相同)。或者,不要修改字典中的项目。将它们键入、删除、修改、重新添加。
  • 很不清楚为什么重写这些方法是必要的。一个好的起点是完全删除 Equals 和 GetHashCode 覆盖,从 Object 继承的默认实现非常好,并保证了对象的唯一性。您永远不会从他们那里收到“重复密钥”错误。
  • 好吧,它不必命名为 Equals() 是吗?您可以随意调用它,Equals() 仅在对 .NET Framework 代码很重要时才需要被覆盖。重要的是,WPF 不太关心 Equals() 返回值在项目绑定时的变化。
  • @HansPassant,如何删除 Equals 和 GetHashCode 覆盖?在 MSDN 上的 IEquatable<T> Interface 页面上,它说 它应该为可能存储在通用集合中的任何对象实现,然后在 IEquatable<T>.Equals Method 页面上它说 如果你实现 Equals,您还应该覆盖 Object.Equals(Object) 和 GetHashCode 的基类实现,以便它们的行为与 IEquatable 的行为一致。

标签: c# class overriding mutable gethashcode


【解决方案1】:

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

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

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

您提出了两种解决方案。

您的第一个解决方案是始终返回一个常量。这满足了这两个规则的要求,但是您将在哈希表中进行线性搜索。您不妨使用一个列表。

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

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

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

【讨论】:

  • +1 感谢您抽出宝贵时间提供可理解的可靠答案。但是,我想谈谈你所说的一些事情......我不在我的代码中使用任何Dictionary 或HashTable anywhere。我使用WPF,我只能假设有问题的HashTable 或Dictionary 在框架内部使用。查看链接的上一个问题中的StackTrace,我看到对System.Windows.DependencyObject.OnPropertyChanged 和后来System.Windows.Controls.ListBoxItem.OnSelected 的调用。所以你看,我无法控制。
【解决方案2】:

我要么创建一个额外的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<T> 的性能特征。这对于熟悉您的班级的人来说可能很明显,但我个人认为这是让人们在脚下开枪。如果您事先知道Dictionary 的行为与预期不同,那么为什么要让用户创建一个呢?在我看来,最好扔NotSupportedException。

但投掷是绝对不能做的!

有些人会不同意上面的说法,当那些人比自己聪明时,就应该注意了。首先,this code analysis warning 声明 GetHashCode 不应该抛出。这是需要考虑的事情,但我们不要教条。有时您必须出于某种原因违反规则。

然而,这还不是全部。在他的blog post on the subject 中,Eric Lippert 说如果你从GetHashCode 里面扔,那么

您的对象不能是许多使用哈希表的 LINQ-to-objects 查询的结果 内部出于性能原因。

失去 LINQ 固然令人遗憾,但幸运的是,这条路并没有就此结束。许多(全部?)使用哈希表的 LINQ 方法具有接受在哈希时使用的 IEqualityComparer<T> 的重载。所以你可以实际上使用 LINQ,但它会不太方便。

最后,您将不得不自己权衡选项。我的观点是,只要在技术上可行,最好使用白名单策略(在需要时提供IEqualityComparer<T>),因为这会使代码明确:如果有人试图天真地使用该类,他们会得到一个有助于说明的异常他们正在发生什么,并且无论在何处使用相等比较器,都可以在代码中看到它,从而使类的异常行为立即清晰。

【讨论】:

  • 好了! +1 我很想尝试回答自己,但想得更好!
  • +1 借调!感谢您花时间写出如此完整的答案@Jon。如果我采用“添加字段”的想法,返回Guid.NewGuid().GetHashCode()(的等效值)可以吗?我不应该在Equals 方法中也使用该字段吗?
  • @Sheridan:这很好,但可能有点矫枉过正。从技术上讲,您也可以使用它在 Equals 内部进行比较,但既然您知道它旨在提供引用相等语义,您也可以直接这样做。
  • 嗨@Jon,在进一步阅读了IEqualityComparer<T> 接口之后,我有点困惑...不会实现该接口,或者按照推荐的那样扩展EqualityComparer<T> 类MSDN,当集合中对象的属性值更改时,会导致与覆盖 Equals 相同的问题?
  • @Sheridan:会的,没有魔法可以让你吃蛋糕也吃。但是看到该类不能按原样工作的过程,创建一个自定义相等比较器并使用它应该足以表明“是的,我知道返回一个常量对性能不利,但已经这样做了” .这个想法是让你的班级的用户明确地选择加入,而不是让他们幸福地“陷入陷阱”。
【解决方案3】:

我想覆盖Equals,但是对于一个对象没有明智的不可变“键”(无论出于何种原因,使整个对象不可变是没有意义的),在我看来只有一个“正确”的选择:

  • 实现GetHashCode 以散列与Equals 使用的字段相同的字段。 (这可能是所有字段。)
  • 记录这些字段在字典中时不得更改。
  • 相信用户要么不将这些对象放入字典中,要么遵守第二条规则。

(返回一个常量值会影响字典性能。抛出异常会禁止太多有用的情况,即对象被缓存但未修改。GetHashCode 的任何其他实现都是错误的。)

无论如何这会给用户带来麻烦,这可能是他们的错。 (特别是:在不应该使用字典的地方使用字典,或者在应该使用使用引用相等的视图模型类型的上下文中使用模型类型。)

或者我一开始就不应该覆盖Equals。

【讨论】:

  • 但是我们希望避免任何用户陷入麻烦,无论故障。
  • 用户只有不遵守规则才会惹上麻烦。除了使类型不可变之外,我没有看到任何有效的替代方案——这也是一个有效的选择,但在某些情况下可能会产生其他后果。
【解决方案4】:

如果类确实不包含可以计算哈希值的常量,那么我会使用比 GUID 更简单的东西。只需使用在类(或包装类)中持久化的随机数。

【讨论】:

  • 谢谢@Dweeberly。请问是什么让您认为这比Guid 有所改进?
  • 生成更小更简单。大多数哈希用法会使用素数修改该值以获取存储桶地址。 GetHashCode 返回一个 32 位的 int,所以任何比这更大的东西都是多余的。
【解决方案5】:

一个简单的方法是将 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 作为键(dictionary[a] = value)将条目存储在字典中。
如果 a 没有改变,则 dictionary[b] 将返回值,但是,如果在将条目存储到字典后更改 a,则 dictionary[b] 很可能会失败。 唯一的解决方法是在任何键更改时重新散列字典。

【讨论】:

  • 感谢@MartinErnst。我在使用这种方法时遇到的问题是,all 的所有类属性都在 Equals 方法中使用,并且它们是 all 可变的。起初我以为我可以在 GetHashCode 中使用 'Id' 和 DateCreated 属性,因为它们永远不会改变,但后来我意识到即使 那些 属性也是可变的 当添加一个新的项目.
  • 您使用的键似乎是实体 - 最简单的方法是使用 id 作为字典中的键,并且在分配新条目之前不要添加新条目,如果这是可能的。或者,如果实体不是新实体,并且如果它是新实体(例如 id 为 0),则可以使用上述方法并仅根据 id 属性生成哈希码,然后使用 base.GetHashCode()。如果您使用新实体作为字典中的键,该字典的寿命超过数据上下文/会话,那么当实体被分配新 id 时,您需要重新散列字典。
  • 感谢您就此事回复我...您能否解释一下您所说的 重新散列字典 是什么意思?此外,对于数据绑定到 WPF ListBox 的集合,这是否可行?
  • 基本上从字典中删除项目并重新添加
猜你喜欢
  • 1970-01-01
  • 2012-08-21
  • 2011-01-08
  • 1970-01-01
  • 1970-01-01
  • 2017-12-28
  • 1970-01-01
  • 2022-01-14
  • 2015-04-01
相关资源
最近更新 更多