良好的GetHashCode()覆盖列表的Foo对象尊重顺序

本文关键字:对象 Foo 顺序 列表 覆盖 GetHashCode | 更新日期: 2023-09-27 18:14:06

EnumerableObject : IEnumerable<Foo>

包裹List<Foo>

如果EnumerableObject a.SequenceEquals( EnumerableObject b),则它们相等。

因此,必须实现GetHashCode。问题是XORing列表中的每个元素对于所有且只有相同元素的列表,无论顺序如何,都将返回相同的哈希码。就其工作而言,这是可以的,但会导致许多冲突,这会减慢检索速度,等等。

对于顺序依赖的对象列表,什么是一个好的、快速的GetHashCode方法?

良好的GetHashCode()覆盖列表的Foo对象尊重顺序

我的做法与我通常组合哈希码的方式相同-加上加法和乘法:

public override int GetHashCode()
{
    unchecked
    {
        int hash = 19;
        foreach (var foo in foos)
        {
            hash = hash * 31 + foo.GetHashCode();
        }
        return hash;
    }
}

(注意,在任何描述的哈希表中的键使用了this之后,不应该向列表中添加任何内容,因为哈希会发生变化。这也假定不存在空条目——如果可能存在,您需要考虑到这一点。

首先,仔细检查您是否需要一个哈希码。你是否打算将这些列表放入一个哈希映射的结构中(例如字典,哈希集等)?如果没有,就忘了它吧。

现在,假设您的意思是EnumerableObject由于某种原因已经覆盖了Equals(object)(因此也有望实现IEquatable<EnumerableObject>),那么这确实是必要的。你想要平衡速度和位分布

一个很好的起点是一个多+加或移位+异,如:

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    foreach(var item in this)
    {
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

(这假设你正在使用item.Equals()来进行序列相等比较,如果你正在使用IEqualityComparer的等号,你需要调用它的哈希码)

从那里我们可以优化。

如果不允许null项,则删除null检查(注意,这将使代码在发现null时抛出)。

如果非常大的列表是常见的,我们需要减少检查的数量,同时尽量不导致大量的冲突。比较以下不同的实现:

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int max = Math.Min(Count, 16);
    for(int i = 0, i != max; ++i)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}
public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int min = Math.Max(-1, Count - 16);
    for(int i = Count -1, i != min; --i)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}
public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int step = Count / 16 + 1;
    for(int i = 0, i < Count; i += step)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

每一个都限制了检查的项的总数,这加快了执行速度,但有降低哈希质量的风险。哪一个(如果有的话)是最好的,取决于集合的起始点相同还是结束点相同的可能性更大。

改变上面的数字16调整余额;越小越快,但越高哈希质量越好,哈希冲突的风险越低。

编辑:现在你可以使用我的SpookyHash v. 2实现:
public override int GetHashCode()
{
  var hasher = new SpookyHash();//use methods with seeds if you need to prevent HashDos
  foreach(var item in this)
    hasher.Update(item.GetHashCode());//or relevant feeds of item, etc.
  return hasher.Final().GetHashCode();
}

这将创建一个比multi +add或shift+xor更好的分布,同时也特别快(特别是在64位进程中,因为算法为此进行了优化,尽管它在32位上也很好)。

.GetHashCode()方法通常只返回一个基于对象引用(指针地址)的哈希值。这是因为计算可枚举列表中每个项的哈希码可能非常耗时。我更喜欢使用扩展方法,而不是覆盖现有的行为,并且只在需要确定哈希码的地方使用它:

public static class EnumerableExtensions
{
    public static int GetSequenceHashCode<TItem>(this IEnumerable<TItem> list)
    {
        if (list == null) return 0;
        const int seedValue = 0x2D2816FE;
        const int primeNumber = 397;
        return list.Aggregate(seedValue, (current, item) => (current * primeNumber) + (Equals(item, default(TItem)) ? 0 : item.GetHashCode()));
    }
}

基于Jon Skeet答案的null处理扩展方法:

#region UTILS
/// <summary>
/// Utils
/// </summary>
internal static class UTILS
{
    #region GetHashCodeByItems
    /// <summary>
    /// Hash code depending on the content and order of the elements of the collection
    /// </summary>
    /// <param name="lst">Collection</param>
    /// <typeparam name="T">The type of items in the collection</typeparam>
    /// <returns>Hash code</returns>
    internal static int GetHashCodeByItems<T>(this IEnumerable<T> lst)
    {
        unchecked
        {
            int hash = 19;
            foreach (T item in lst)
            {
                hash = hash * 31 + (item != null ? item.GetHashCode() : 1);
            }
            return hash;
        }
    }
    #endregion
}
#endregion