覆盖 GetHashCode() 方法

20

这篇文章中,Jon Skeet提到他通常使用这种算法来重载GetHashCode()方法。

public override int GetHashCode()
{
  unchecked // Overflow is fine, just wrap
  {
    int hash = 17;
    // Suitable nullity checks etc, of course :)
    hash = hash * 23 + Id.GetHashCode();
    return hash;
  }
}

我尝试使用这个方法,但是Resharper告诉我应该只使用只读字段来进行哈希,而我的字段并不是只读的(虽然代码可以正常编译)。那么有什么好的实践方法呢?因为现在我不能将我的字段设置为只读。

我尝试通过Resharper生成这个方法,以下是结果。

public override int GetHashCode()
{
  return base.GetHashCode();
}

说实话,这并没有太大的贡献...


重写 GetHashCode - Ria
2
@Ria 这个问题中没有人谈论只读字段。 - Pacane
7
您可以参考Eric Lippert博客上的这篇文章。特别是注意其中的一节“规则:在依赖哈希码保持稳定的数据结构中包含对象时,由GetHashCode返回的整数值不能更改”。他写道,“可以创建这样的对象,使其哈希码值会随着对象字段的变化而变化,但这样做虽然被允许,但却是危险的”。 - drf
3个回答

18

如果你的所有字段都是可变的,并且你必须实现GetHashCode方法,那么我恐怕这就是你需要拥有的实现。

public override int GetHashCode() 
{ 
    return 1; 
} 

是的,这种方法很低效,但至少是正确的。

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

请注意,所有对象返回相同的哈希码1,这意味着所有对象都被放置在HashSet或Dictionary中的同一个桶中。因此,HashSet或Dictionary中始终只有一个桶。当试图查找对象时,它将对桶内的每个对象进行相等性检查。这就像在链表中进行搜索。

有人可能会认为,如果我们可以确保在将对象添加到HashCode或Dictionary集合后永远不会更改字段,则基于可变字段实现哈希码是可以接受的。我个人认为这很容易出错。两年后接手您的代码的人可能不知道此事并在意外情况下破坏代码。


strucs怎么办?字段也必须是不可变的吗?我发现在存储在Dictionary中时很难更改它们的字段。 - John Alexiou
1
如果我们使用默认的GetHashCode(),那么两个具有相同字段Id=1的对象将具有不同的HashCode,因此从不同的桶中查找。我认为在这种情况下,我们希望它们具有相同的HashCode。 - Harvey Kwok
1
@HarveyKwok:这实际上取决于对象是否可比较(其中相等性基于可变字段的相等性)。如果是,则您是正确的,默认的GetHashCode不适用。如果不是,对象的身份不取决于其(可变)字段的值,因此默认的GetHashCode就可以了。但是,基于可变字段计算GetHashCode也不是一个好主意,因此可变可比较对象也不是一个好主意。 - Anton Tykhyy
@Eamon:我认为这也不是一个好的答案。如果查找性能是一个问题,我个人在我的项目中不会这样做。问题在于,在C#中没有简单的方法来保证字段在用作键时是不可变的。我知道的最接近的方法是WPF中的Freezable对象。您可能需要类似或比此更灵活的东西。您不希望说程序员将确保在使用它们时字段是不可变的。代码将非常容易出错。 - Harvey Kwok
能够实际地强制执行不可变性是很好的,但仅仅因为它不能这样做并不意味着放弃一切并假设所有内容都被改变是合理的。编写有效的C#应用程序需要满足许多无法证明的代码约定,而“构建后永不更改”的(相当简单的)约定并不是不合理的。人们在完全动态的语言中制作了大型应用程序;缺乏静态验证是有问题的,但并非致命的。在这里,类似于C#的解决方案是接受在C#中实际上无法强制执行不可变性,并继续前进。 - Eamon Nerbonne
显示剩余4条评论

5
请注意,您的GetHashCode方法必须与Equals方法相互配合。如果您可以仅使用引用相等性(当您永远不会有两个不同的类实例可以相等时),则可以安全地使用从Object继承的Equals和GetHashCode方法。这比仅从GetHashCode中返回1要好得多。

-2
我个人倾向于在没有不可变字段的类中,为每个GetHashCode()实现返回不同的数字值。这意味着如果我有一个包含不同实现类型的字典,不同类型的不同实例有可能被放置在不同的桶中。
例如:
public class A
{
    // TODO Equals override

    public override int GetHashCode()
    {
        return 21313;
    } 
}

public class B
{
    // TODO Equals override

    public override int GetHashCode()
    {
        return 35507;
    } 
}

如果我有一个包含AB和其他类型实例的Dictionary<object, TValue>,那么查找的性能将比所有GetHashCode实现返回相同数字值的情况要好。

还应该注意,我使用质数来获得更好的分布。

根据评论,我提供了一个LINQPad示例here,演示了对不同类型使用return 1和为每种类型返回不同值之间的性能差异。


你能详细说明一下你最后一句话的意思吗? - jwg
回想起这个答案,最后一句话并不适用于这种情况。然而,在实现具有只读字段的 GetHashCode() 方法时,使用质数可以确保更好地分布在字典或哈希集的桶中。有很多在线文章,但是这个SO问题应该能够帮助您入门。 - Lukazoid
解释一下“downvote”会很好,我觉得这是处理不同类型时采取的合理方法。 - Lukazoid
@Lukazoid 抱歉,我通常会添加注释 - 这次忘记了! - Eamon Nerbonne
@Lukazoid:这种解决方案的问题在于它鼓励O(N^2)的行为。不同类之间缺乏冲突几乎没有帮助,只有在代码混合类型并破坏类型系统的情况下才有帮助——也就是你真的不想费心帮助的代码。 - Eamon Nerbonne
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接