【问题标题】:Concise way to combine field hashcodes?组合字段哈希码的简洁方法?
【发布时间】:2023-03-08 17:12:01
【问题描述】:

Jon Skeet here 概述了实现 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);  
   }  

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

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

【问题讨论】:

  • 据我所知,最接近的方法是使用 ReSharper 进行相等成员和哈希码生成...
  • .NET 中的所有对象都实现了GetHashCode(),如果你想将它们组合起来,只需将你想要使用的任何逻辑放在一个辅助方法中。
  • @evanmcdonnal,我不想写辅助方法。我希望其他人编写一个标准的辅助方法。具体来说,我希望有人编写一个 correct 辅助方法,以尽量减少编写(或维护者将其更改为)不正确实现的机会。以一种导致许多冲突的方式错误地组合哈希非常容易。
  • @HighCore,重写 hashcode/GetHashCode 的原因有很多。特别是,如果您覆盖 Equals 并且如果您的对象将成为哈希表中的键,则如果您想要合理的行为,则建议您这样做。 msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx
  • @bacar Hashtable was 8 years ago,你也需要一个很好的理由来覆盖Equals()

标签: c# java hash


【解决方案1】:

有些人使用:

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

提到了on MSDN at Object.GetHashCode(),并带有警告:

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

聚合组成哈希的逻辑由System.Tuple 提供,希望对此有所考虑...

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

【讨论】:

  • 嗯,我敢打赌这可能做得很好。我想知道元组中的 n 有多大?我怀疑实现 Java 风格的解决方案所需的 4 行代码有什么大不了的,但我想我可以理解对标准且易于理解的解决方案的渴望。
  • @ebyrob 在 C# 中最大的是一个 8 元组。
  • Tuple.Create(first, second, third, fourth, fifth, sixth, seventh, Tuple.Create(eight, ninth, tenth, ...)).GetHashCode() 能解决这个问题吗?
  • @bacar 是的,但它的效率不是很高,而且哈希码生成应该是一种高效的操作。 OP 描述的方法也很容易正确实现。
  • @Servy 你会这么认为的,对吧?我确实遇到了一个错误的实现,因此是动机。有很多错误你可能会不小心放在这里 - 交换加法/乘法,乘数选择不当,完全忘记加法部分......我已经看到它们发生了,最糟糕的是它们看起来 类似于“标准”解决方案,最终也通过了代码审查。我认为您应该在解决瓶颈的地方自行开发,但在没有解决的地方尽量减少维护人员的认知负担。
【解决方案2】:

不完全一样,但我们在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;
}

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

【讨论】:

  • +1。虽然我仍然觉得你只在那些特殊情况下才需要这个,而不是日常的public class Personpublic class BillingRepository 等。
  • @HighCore:看似特殊的情况可能是其他人的日常工作。不是每个人都编写相同类型的代码。
  • @automatonic:不用了,恐怕……现在修好了。
  • 不错的一个。我发现这个添加很有用:internal static int HashAll(params object[] values) { int initialHash = Initialize(); return values.Aggregate(initialHash, Hash); }
  • @anjdreas:是的,但这意味着 a) 每次都创建一个数组; b) 装箱值类型。
【解决方案3】:
public override GetHashCode()
{
    return this.Field1.GetHashCode() | this.Field2.GetHashCode | this.Field3.GetHashCode();
}

【讨论】:

  • 这会产生一个很差的哈希值,并且有很多冲突; {Field1="foo",Field2="bar"} 生成与{Field1="bar",Field2="foo"} 相同的哈希值。另外,使用 or 而不是 Xor 可能会被视为特别糟糕 - 你拥有的字段越多,你的哈希值越有可能等于 0xFFFFFFFF - 事实上,如果Field1.GetHashCode()= -1 = 0xFFFFFFFF,没关系其他字段是什么,它们的哈希码都是 0xFFFFFF。
  • 并注意 int32 的 GetHashCode 只是返回值本身。因此,如果 Field1 为 -1,则您的所有其他字段都将在此实现中变得多余。
  • 即使return (this.Field1.GetHashCode().ToString() + this.Field2.GetHashCode().ToString()).GetHashCode(); 也会做出更好的实现。 ;-)
【解决方案4】:

编辑: 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 现在有一个 Quick Actions 帮助器可以为您生成 Equals()GetHashCode() 只需右键单击声明中的类名 > Quick Actions and Refactorings > Generate Equals 和 GetHashCode .选择您希望它用于相等的成员,以及“Implement IEquatable”,然后单击“确定”。

最后一件事:如果您需要获取对象的 structural 哈希码,例如,如果您想包含一个数组的哈希码,该数组会根据其 内容而发生变化 em>(又名结构)而不是它的 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 Reference source 第 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));
}

当然,实际的元组GetHashCode()(实际上是一个Int32 IStructuralEquatable.GetHashCode(IEqualityComparer comparer))有一个很大的switch 块来根据它持有的项目数来决定调用其中的哪一个——你自己的代码可能会赢不需要。

【讨论】:

  • 请注意HashCodeCombiner以a seed of 5381开头
  • "System.Tuple 和其他不可变复合类型也将在后台使用它。"它现在在 netcore 2.1 中。请注意,BCL(元组等)还没有使用它,因为我在 netfx 下工作时遇到了很多问题——这可能只会在下一个 netfx 版本中/之后出现。
【解决方案5】:

这里是 Ryan 的 answer 中提到的 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);
    }

【讨论】:

    猜你喜欢
    • 2010-09-29
    • 2011-11-06
    • 2010-12-11
    • 2010-10-24
    • 1970-01-01
    • 2011-04-04
    • 2011-02-25
    • 2013-12-14
    • 2018-06-05
    相关资源
    最近更新 更多