人们能否推荐快速简单的方法来组合两个对象的哈希码。我没有太担心冲突,因为我有一个哈希表,该哈希表可以有效地处理该问题,我只希望有一些东西可以尽快生成代码。
围绕SO和Web进行阅读似乎有一些主要的候选人:
- 异或
- 使用素数乘法进行异或
- 简单的数字运算,例如乘法/除法(带有溢出检查或环绕)
- 生成一个String,然后使用String类的Hash Code方法
人们会推荐什么,为什么?
Answers:
我个人会避免XOR-这意味着任何两个相等的值都将导致0-因此hash(1,1)== hash(2,2)== hash(3,3)等。另外hash(5,0) == hash(0,5)等可能偶尔出现。我已经刻意用它集合散列-如果你想哈希项目的顺序,你不关心的排序,这是不错的。
我通常使用:
unchecked
{
int hash = 17;
hash = hash * 31 + firstField.GetHashCode();
hash = hash * 31 + secondField.GetHashCode();
return hash;
}
这就是Josh Bloch在Effective Java中建议的形式。上一次我回答类似的问题时,我设法找到了一篇文章进行了详细的讨论-IIRC,没有人真正知道为什么它行之有效,但确实如此。它也很容易记住,易于实现,并且易于扩展到任意数量的字段。
unchecked { }
块中。GetHashCode()不应引发任何异常。
尽管在Jon Skeet的答案中概述的模板通常可以很好地作为哈希函数系列使用,但常量的选择很重要,答案中指出的种子17
和因数31
对于普通用例根本不起作用。在大多数使用情况下,散列的值比都更接近零int.MaxValue
,并且共同进行散列的项目数不超过几十个。
对于散列一个整数的元组{x, y}
,其中-1000 <= x <= 1000
和-1000 <= y <= 1000
,它有将近98.5%的深不可测的碰撞率。例如{1, 0} -> {0, 31}
,{1, 1} -> {0, 32}
等等。如果我们扩大覆盖范围还包括n元组在那里3 <= n <= 25
,但它确实不太可怕的约38%的碰撞率。但是我们可以做得更好。
public static int CustomHash(int seed, int factor, params int[] vals)
{
int hash = seed;
foreach (int i in vals)
{
hash = (hash * factor) + i;
}
return hash;
}
我写了一个蒙特卡洛采样搜索循环,用随机种子的各个随机n元组的各种种子和因子值测试了上述方法i
。允许的范围为2 <= n <= 25
(n
是随机的,但偏向范围的下限)和-1000 <= i <= 1000
。每个种子和因子对至少进行了1200万次唯一的碰撞测试。
运行大约7小时后,最好对发现(其中种子和因子均被限制为4位数字或更少)为:seed = 1009
,factor = 9176
,用0.1131%的碰撞率。在5位数和6位数区域,甚至存在更好的选择。但是为了简洁起见,我选择了性能最高的4位数字,并且在所有常见int
和char
哈希情况下,它的表现都很好。对于更大的整数,它似乎也可以正常工作。
值得注意的是,“成为主要人物”似乎并不能作为取得种子和/或因素良好表现的一般先决条件,尽管它可能会有所帮助。1009
上面提到的实际上是素数,但9176
不是。我明确测试了这种变化,在factor
附近9176
(离开seed = 1009
)更改为各种素数,它们的表现都比上述解决方案差。
最后,我还比较了通用的ReSharper推荐功能系列hash = (hash * factor) ^ i;
和CustomHash()
上面提到的原始功能,它们的性能要好得多。对于常见的用例假设,ReSharper XOR样式的冲突率似乎在20-30%的范围内,我认为不应使用。
params int[] vals
必须位于所有函数参数的末尾,所以我无法设置seed
和factor
默认参数。如果您不关心params
语法的便利性,则可以将其删除,然后对参数重新排序以允许按照建议使用默认值。
HashSet<int>
参数(假设没有重复项)。否则,您可以重命名该功能以CustomHashCombination()
防止混淆,并按照建议进行内部预排序。
params
因为它必须在每次调用时分配一个数组。因此,它可能是更快的计算方法,但会为以后创建GC压力。
如果您使用的是.NET Core 2.1或更高版本或.NET Framework 4.6.1或更高版本,请考虑使用System.HashCode结构来帮助生成复合哈希码。它具有两种操作模式:添加和合并。
使用的示例Combine
通常更简单,最多可处理八个项目:
public override int GetHashCode()
{
return HashCode.Combine(object1, object2);
}
使用示例Add
:
public override int GetHashCode()
{
var hash = new HashCode();
hash.Add(this.object1);
hash.Add(this.object2);
return hash.ToHashCode();
}
优点:
IEqualityComparer
实例的重载缺点:
HashCode
是.NET Standard 2.1的一部分。截至2019年9月,.NET团队尚无计划在.NET Framework上支持.NET Standard 2.1,因为.NET Core / .NET 5是.NET的未来。在元组中使用组合逻辑。该示例使用c#7元组。
(field1, field2).GetHashCode();
ValueTuple
是结构(MSDN)的类型。请注意,该Tuple
类型是一类,并且具有GC压力。我喜欢这样 在内部它类似于@Stipo的帖子,但非常易于理解和查看。在大多数情况下,这将是一个不错的选择。
我假设.NET Framework团队在测试他们的System.String.GetHashCode()实现方面做得不错,所以我将使用它:
// System.String.GetHashCode(): http://referencesource.microsoft.com/#mscorlib/system/string.cs,0a17bbac4851d0d4
// System.Web.Util.StringUtil.GetStringHashCode(System.String): http://referencesource.microsoft.com/#System.Web/Util/StringUtil.cs,c97063570b4e791a
public static int CombineHashCodes(IEnumerable<int> hashCodes)
{
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
int i = 0;
foreach (var hashCode in hashCodes)
{
if (i % 2 == 0)
hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ hashCode;
else
hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ hashCode;
++i;
}
return hash1 + (hash2 * 1566083941);
}
另一个实现来自System.Web.Util.HashCodeCombiner.CombineHashCodes(System.Int32,System.Int32)和System.Array.CombineHashCodes(System.Int32,System.Int32)方法。这个比较简单,但是可能没有上面的方法那样好的分布:
// System.Web.Util.HashCodeCombiner.CombineHashCodes(System.Int32, System.Int32): http://referencesource.microsoft.com/#System.Web/Util/HashCodeCombiner.cs,21fb74ad8bb43f6b
// System.Array.CombineHashCodes(System.Int32, System.Int32): http://referencesource.microsoft.com/#mscorlib/system/array.cs,87d117c8cc772cca
public static int CombineHashCodes(IEnumerable<int> hashCodes)
{
int hash = 5381;
foreach (var hashCode in hashCodes)
hash = ((hash << 5) + hash) ^ hashCode;
return hash;
}
这是Special Sauce出色研究解决方案的重新包装。
它利用值元组(ITuple
)。
这允许使用参数seed
和的默认值factor
。
public static int CombineHashes(this ITuple tupled, int seed=1009, int factor=9176)
{
var hash = seed;
for (var i = 0; i < tupled.Length; i++)
{
unchecked
{
hash = hash * factor + tupled[i].GetHashCode();
}
}
return hash;
}
用法:
var hash1 = ("Foo", "Bar", 42).CombineHashes();
var hash2 = ("Jon", "Skeet", "Constants").CombineHashes(seed=17, factor=31);