简洁的方法合并字段哈希码?

33

如果需要实现 GetHashCode 的一种方法可以参考 Jon Skeet 在这里的代码。重复他的代码:

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来生成相等成员和哈希码。 - Patryk Ćwiek
在.NET中,所有对象都实现了 GetHashCode() 方法,如果你想合并它们,只需将任何逻辑放入帮助方法即可。 - evanmcdonnal
2
@evanmcdonnal,我不想编写一个辅助方法。我希望有人编写一个标准的辅助方法。特别是我希望有人编写一个_正确_的辅助方法,以尽量减少编写(或维护人员更改)错误实现的可能性。将哈希值错误地组合起来,导致碰撞的机会很大。 - bacar
3
@HighCore,有许多理由可以覆盖 hashcode/GetHashCode。特别是当您覆盖 Equals 时建议这样做,如果您的对象将成为哈希表中的键,则必须这样做以获得合理的行为。http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx - bacar
@bacar Hashtable 是8年前的技术了,覆盖Equals()方法需要有充分的理由。 - Federico Berasategui
显示剩余8条评论
5个回答

24

编辑: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”,然后单击确定。

最后一件事:如果您需要获取对象的结构哈希码,例如如果您想要包括基于其内容(即结构)而不是引用更改的数组的哈希码,则需要将字段强制转换为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));
}

当然了,实际上元组 GetHashCode() (实际上是一个 Int32 IStructuralEquatable.GetHashCode(IEqualityComparer comparer))有一个大的 switch 块来决定调用哪一个,基于它持有多少项 - 您自己的代码可能不需要这样。


1
请注意,HashCodeCombiner从种子值5381开始。 - Gyum Fox
它还将被System.Tuple和其他不可变复合类型在幕后使用。它现在在netcore 2.1中。请注意,BCL(Tuple等)目前尚未使用它,因为我在netfx下使用它时遇到了巨大的问题 - 这可能只会随着/在下一个版本的netfx之后才会出现。 - Jonathan Dickinson

20

有些人使用:

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

MSDN的Object.GetHashCode()中提到,警告如下:

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

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

更新: 值得注意的是@Ryan在评论中的观察结果,当Tuple的大小>8时,这似乎只使用最后8个元素。


1
嗯,我敢打赌这个应该表现得很好。我想知道元组中的n可以有多大?我怀疑实现Java风格解决方案所需的4行代码并不是什么大问题,但我可以理解对于一个标准且易于理解的解决方案的渴望。 - user645280
1
@ebyrob,C#中最大的是8元组。 - evanmcdonnal
Tuple.Create(first, second, third, fourth, fifth, sixth, seventh, Tuple.Create(eight, ninth, tenth, ...)).GetHashCode() 处理这个问题吗? - bacar
1
@bacar 是的,但它并不是非常高效,而哈希码生成应该是一种高效的操作。OP所描述的方法也足够容易实现。 - Servy
2
@Servy 你会这样想,是吗?我确实遇到了一个有缺陷的实现,因此才有了动力。你可能会无意中引入很多错误 - 交换加法/乘法,选择不当的乘数,完全忘记加法部分...我见过它们发生,最糟糕的部分是它们看起来与“标准”解决方案有点相似,并且最终通过了代码审查。我认为应该在解决瓶颈的地方自己编写代码,但在不需要时要尽量减少维护者的认知负担。 - bacar
显示剩余5条评论

10

虽然不完全相同,但我们在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使用了检查算术作为项目设置,而哈希码计算应该允许溢出。

尽管我仍然觉得你只需要在那些特殊情况下使用它,而不是用于正常日常的public class Personpublic class BillingRepository等。 - Federico Berasategui
7
可能看起来是个特例的情况,对其他人来说可能是日常工作。并非每个人都写同样类型的代码。 - Jon Skeet
@automatonic:恐怕没有了...现在已经修复了。 - Jon Skeet
1
不错。我发现这个添加很有用: internal static int HashAll(params object[] values) { int initialHash = Initialize(); return values.Aggregate(initialHash, Hash); } - angularsen
@anjdreas:对,但这意味着a)每次都要创建一个数组;b)将值类型装箱。 - Jon Skeet
显示剩余2条评论

1

以下是对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);
    }

-9
public override GetHashCode()
{
    return this.Field1.GetHashCode() | this.Field2.GetHashCode | this.Field3.GetHashCode();
}

这会产生很多冲突的非常差的哈希值;{Field1="foo",Field2="bar"}会生成与{Field1="bar",Field2="foo"}相同的哈希值。此外,使用或运算而不是异或运算可能被视为特别糟糕——字段越多,哈希值等于0xFFFFFFFF的可能性就越大——实际上,如果 Field1.GetHashCode()=-1= 0xFFFFFFFF,那么其他所有字段的哈希码都将为0xFFFFFF,而其他字段的哈希值将无关紧要。 - bacar
请注意,int32的GetHashCode只返回值本身。因此,如果Field1为-1,则此实现将使您的所有其他字段都变得多余。 - bacar
即使return (this.Field1.GetHashCode().ToString() + this.Field2.GetHashCode().ToString()).GetHashCode();会更好一些。;-) - Mitja

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