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

时间:2013-10-31 15:04:28

标签: c# class override mutable gethashcode

好的,在你因为互联网上发布了数百个类似的听起来问题之前我已经疯了,我可以向你保证,我刚刚花了几个小时阅读所有这些而没有找到了我的问题的答案。

背景

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

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

现在,对于那些不了解此问题的人,我发现您只能使用 immutable 字段/属性来实现GetHashCode方法。使用Harvey Kwok对Overriding GetHashCode()帖子的回答摘录来解释:

  

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

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

问题:

所以,我的问题是处理我需要在没有不可变字段的类中实现GetHashCode方法的情况的最佳方法是什么?抱歉,让我更具体一点,因为

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

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

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

要点:

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

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

5 个答案:

答案 0 :(得分:14)

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

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

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

您提出两种解决方案。

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

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

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

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

答案 1 :(得分:3)

我要么创建一个额外的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到对象查询的结果   内部因性能原因。

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

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

答案 2 :(得分:1)

在我想覆盖Equals的地方,但是对于对象没有明智的不可变的“键”(无论出于什么原因,使整个对象不可变都是没有道理的),在我看来只有一个“正确”选择:

  • 实施GetHashCode以哈希与Equals使用的相同字段。 (这可能是所有字段。)
  • 说明在字典中这些字段不得更改。
  • 相信用户要么不将这些对象放在字典中,要么服从第二条规则。

(返回常数会损害字典性能。如果抛出异常,则会在许多有用的情况下缓存对象但未对其进行修改。GetHashCode的任何其他实现都是错误的。)

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

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

答案 3 :(得分:0)

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

答案 4 :(得分:0)

一种简单的方法是将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]很可能会失败。 唯一的解决方法是在任何键更改时重新发送字典。