组合字段哈希代码的简洁方式

本文关键字:简洁 方式 代码 字段 哈希 组合 | 更新日期: 2023-09-27 18:26:27

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 + field1.GetHashCode();
        hash = hash * 23 + field2.GetHashCode();
        hash = hash * 23 + field3.GetHashCode();
        return hash;
    }
}

手动滚动这些代码可能很容易出错,错误可能很微妙/很难发现(你是否错误地交换了+*。它还可以混淆重复噪声中最重要的细节之一(我记得包括所有字段吗?)。

是否有一种使用.net库组合字段哈希代码的简洁方法。很明显,我可以自己写,但如果有惯用的/内置的东西,我更喜欢。

举个例子,在Java中(使用JDK7),我可以使用来实现上述功能

   @Override
   public int hashCode()  
   {  
      return Objects.hash(field1, field2, field3);  
   }  

这确实有助于消除错误并关注重要的细节。

动机:我遇到了一个C#类,它需要一个重写的GetHashCode(),但它组合各种组成部分的哈希代码的方式有一些严重的错误。用于组合哈希代码的库函数对于避免此类错误非常有用。

组合字段哈希代码的简洁方式

编辑:System.HashCode现已发布。现在推荐的创建哈希代码的方法是:

public override int GetHashCode()
{
    return HashCode.Combine(fieldA, fieldB, fieldC);
}

System.HashCode.Combine()会在每个字段内部调用.GetHashCode(),并自动执行正确的操作。

对于很多字段(超过8个),您可以创建HashCode的实例,然后使用.Add()方法:

public override int GetHashCode()
{
    HashCode hash = new HashCode();
    hash.Add(fieldA);
    hash.Add(fieldB);
    hash.Add(fieldC);
    hash.Add(fieldD);
    hash.Add(fieldE);
    hash.Add(fieldF);
    hash.Add(fieldG);
    hash.Add(fieldH);
    hash.Add(fieldI);
    return hash.ToHashCode();
}

Visual Studio 2019现在有了一个"快速操作"帮助程序,可以为您生成Equals()GetHashCode()只需右键单击声明中的类名>快速操作和重构>生成Equals和GetHashCode。选择您希望它用于相等的成员,以及";实现IEquatable";,然后单击"确定"。

最后一件事:如果你需要获得对象的结构哈希代码,例如,如果你想包括一个数组的哈希代码,该数组根据其内容(又名结构)而不是其reference而变化,那么你需要将字段强制转换为IStructuralEquatable并手动获取其哈希代码,如下所示:

public override int GetHashCode()
{
    return HashCode.Combine(
        fieldA,
        ((IStructuralEquatable)stringArrayFieldB).GetHashCode(EqualityComparer<string>.Default));
}

这是因为IStructuralEquatable接口几乎总是显式实现的,因此需要转换为IStructuralEquatable来调用IStructuralEquatable.GetHashCode(),而不是默认的object.GetHashCode()方法。

最后,在当前实现中,int.GetHashCode只是整数值本身,因此将散列码值传递给HashCode.Combine()而不是字段本身对结果没有影响。

旧答案:

为了完整起见,这里是从.NET Tuple参考源第52行获取的哈希算法。有趣的是,这个散列算法是从System.Web.Util.HashCodeCombiner复制过来的。

这是代码:

public override int GetHashCode() {
    // hashing method taken from .NET Tuple reference
    // expand this out to however many items you need to hash
    return CombineHashCodes(this.item1.GetHashCode(), this.item2.GetHashCode(), this.item3.GetHashCode());
}
internal static int CombineHashCodes(int h1, int h2) {
    // this is where the magic happens
    return (((h1 << 5) + h1) ^ h2);
}
internal static int CombineHashCodes(int h1, int h2, int h3) {
    return CombineHashCodes(CombineHashCodes(h1, h2), h3);
}
internal static int CombineHashCodes(int h1, int h2, int h3, int h4) {
    return CombineHashCodes(CombineHashCodes(h1, h2), CombineHashCodes(h3, h4));
}
internal static int CombineHashCodes(int h1, int h2, int h3, int h4, int h5) {
    return CombineHashCodes(CombineHashCodes(h1, h2, h3, h4), h5);
}
internal static int CombineHashCodes(int h1, int h2, int h3, int h4, int h5, int h6) {
    return CombineHashCodes(CombineHashCodes(h1, h2, h3, h4), CombineHashCodes(h5, h6));
}
internal static int CombineHashCodes(int h1, int h2, int h3, int h4, int h5, int h6, int h7) {
    return CombineHashCodes(CombineHashCodes(h1, h2, h3, h4), CombineHashCodes(h5, h6, h7));
}
internal static int CombineHashCodes(int h1, int h2, int h3, int h4, int h5, int h6, int h7, int h8) {
    return CombineHashCodes(CombineHashCodes(h1, h2, h3, h4), CombineHashCodes(h5, h6, h7, h8));
}

当然,实际的Tuple GetHashCode()(实际上是一个Int32 IStructuralEquatable.GetHashCode(IEqualityComparer comparer))有一个大的switch块,可以根据它包含的项目数量来决定调用其中的哪一个——您自己的代码可能不需要这样做。

有些人使用:

Tuple.Create(lastName, firstName, gender).GetHashCode()

MSDN上的Object.GetHashCode()中提到了它,并警告:

不过,请注意,实例化Tuple对象的性能开销可能会显著影响在哈希表中存储大量对象的应用程序的整体性能。

System.Tuple提供了聚合组成哈希的逻辑,希望它已经对此进行了一些思考…

更新:值得注意的是@Ryan在评论中观察到,这似乎只使用了任何大小>8的元组的最后8个元素。

这并不完全相同,但我们在Noda Time中有一个HashCodeHelper类(它有很多类型可以覆盖相等和哈希码操作)。

它是这样使用的(取自ZonedDateTime):

public override int GetHashCode()
{
    int hash = HashCodeHelper.Initialize();
    hash = HashCodeHelper.Hash(hash, LocalInstant);
    hash = HashCodeHelper.Hash(hash, Offset);
    hash = HashCodeHelper.Hash(hash, Zone);
    return hash;
}

请注意,这是一个通用方法,可以避免对值类型进行装箱。它会自动处理null值(使用0作为值)。请注意,MakeHash方法有一个unchecked块,因为Noda Time使用检查算术作为项目设置,而哈希代码计算应该允许溢出。

以下是Ryan的回答中提到的System.Web.Util.HashCodeCombiner的几个简洁(尽管没有那么有效)的重构

    public static int CombineHashCodes(params object[] objects)
    {
        // From System.Web.Util.HashCodeCombiner
        int combine(int h1, int h2) => (((h1 << 5) + h1) ^ h2);
        return objects.Select(it => it.GetHashCode()).Aggregate(5381,combine);
    }
    public static int CombineHashCodes(IEqualityComparer comparer, params object[] objects)
    {
        // From System.Web.Util.HashCodeCombiner
        int combine(int h1, int h2) => (((h1 << 5) + h1) ^ h2);
        return objects.Select(comparer.GetHashCode).Aggregate(5381, combine);
    }
public override GetHashCode()
{
    return this.Field1.GetHashCode() | this.Field2.GetHashCode | this.Field3.GetHashCode();
}