这个问题是关于Jon Skeet在这个问题上给出的答案:" What is the best algorithm for an overridden System.Object.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;
}
}
我不明白为什么选择数字17和23。为什么我们不选3和5?这也是素数。 有人可以解释一下最好的素数是什么以及为什么?
答案 0 :(得分:10)
您链接到的答案的评论已经简要地尝试解释为什么17
和23
不适合在这里使用。
许多使用哈希码的.NET类存储桶中的元素。假设有三个桶。然后,所有具有哈希码0,3,6,9 ......的对象都存储在桶0中。所有具有哈希码1,4,7,10 ......的对象都存储在桶1中。所有带桶2的对象,5,8,11 ......存放在桶2中。
现在假设您的GetHashCode()
使用hash = hash * 3 + field3.GetHashCode();
。这意味着除非hash
足够大以使乘法环绕,否则在具有三个存储桶的哈希集中,对象最终会进入的存储桶仅取决于field3
。
如果对象在桶中分布不均匀,HashSet<T>
无法提供良好的性能。
您想要一个与所有可能数量的存储桶共存的因子。由于相同的原因,桶本身的数量将是素数,因此如果您的因子是素数,唯一的风险是它等于桶的数量。
.NET使用a fixed list of allowed numbers of buckets:
public static readonly int[] primes = { 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369};
您的因素应该是.NET不能使用的因素,而其他自定义实现同样不太可能使用。这意味着23
是一个不好的因素。 31
可以使用.NET自带的容器,但对于自定义实现可能同样糟糕。
同时,它不应该太低,以至于它会为常见用途提供大量碰撞。这是3
和5
的风险:假设您有一个包含大量小整数的自定义Tuple<int, int>
实现。请注意,int.GetHashCode()
只返回int
本身。假设您的乘法因子是3
。这意味着(0, 9)
,(1, 6)
,(2, 3)
和(3, 0)
都会提供相同的哈希码。
使用足够大的素数可以避免这两个问题,正如Jon Skeet在他的回答中引用的评论所指出的那样:
编辑:正如评论中所述,您可能会发现选择较大的素数来代替它会更好。显然486187739很好......
曾几何时,用于乘法的大质数可能是坏的,因为大整数的乘法足够缓慢,以至于性能差异是明显的。 31
的乘法在这种情况下会很好,因为它可以实现为x * 31
=&gt; x * 32 - x
=&gt; (x << 5) - x
。然而,如今,乘法不太可能导致任何性能问题,然后,一般来说,越大越好。