是否可以将私有成员的哈希码组合起来生成新的哈希码?

时间:2009-07-03 12:43:03

标签: c# hashcode gethashcode

我有一个对象,我想生成一个唯一的哈希(覆盖GetHashCode()),但我想避免溢出或不可预测的事情。

代码应该是组合一小组字符串的哈希码的结果。

哈希码将是生成缓存密钥的一部分,所以理想情况下它们应该是唯一的,但是被散列的可能值的数量很小所以我认为概率对我有利。

这样的事情是否足够并且有更好的方法吗?

int hash = 0;
foreach(string item in collection){
    hash += (item.GetHashCode() / collection.Count)
}
return hash;
编辑:到目前为止,感谢您的回答。 @Jon Skeet:不,订单不重要

我想这几乎是另一个问题,但由于我使用结果生成缓存键(字符串),使用像MD5这样的加密哈希函数还是只使用这个int的字符串表示有意义?

4 个答案:

答案 0 :(得分:24)

哈希不是意味着是唯一的 - 它们只是意味着在大多数情况下都能很好地分发。它们只是意味着一致。请注意,溢出应该不是问题。

只是添加通常不是一个好主意,而分开肯定不是。这是我经常使用的方法:

int result = 17;
foreach (string item in collection)
{
    result = result * 31 + item.GetHashCode();
}
return result;

如果您处于已检查的上下文中,则可能需要故意将其取消选中。

请注意,这假定顺序很重要,即{“a”,“b”}应与{“b”,“a”}不同。如果情况不是这样,请告诉我们。

答案 1 :(得分:24)

Marc和Jon指出的基本面并不差,但就结果分布的均匀性而言,它们远非最优。遗憾的是,来自Knuth的许多人复制的“乘以素数”的方法是not the best choice in many cases更好的分布可以通过更便宜的计算函数来实现(尽管这在现代硬件上非常很少)。事实上,将素数投入哈希的许多方面都是no panacea

如果此数据用于大小合适的哈希表,我建议使用c#轻松读取Bret Mulvey's excellent study and explanation of various modern (and not so modern) hashing techniques

请注意,使用各种散列函数的字符串的行为严重偏向于字符串很短(粗略地说是在位开始溢出之前散列了多少个字符)或长。

最简单,最容易实现的一个也是最好的之一,Jenkins One一次散列。

private static unsafe void Hash(byte* d, int len, ref uint h)
{
    for (int i = 0; i < len; i++)
    {
        h += d[i];
        h += (h << 10);
        h ^= (h >> 6);
    }
}

public unsafe static void Hash(ref uint h, string s)
{
    fixed (char* c = s)            
    {
        byte* b = (byte*)(void*)c;
        Hash(b, s.Length * 2, ref h);
    }
}

public unsafe static int Avalanche(uint h)
{
    h += (h<< 3);   
    h ^= (h>> 11);  
    h += (h<< 15);  
    return *((int*)(void*)&h);
}

然后您可以像这样使用它:

uint h = 0;
foreach(string item in collection) 
{
    Hash(ref h, item);
}
return Avalanche(h);

您可以合并多种不同类型,如下所示:

public unsafe static void Hash(ref uint h, int data)
{ 
    byte* d = (byte*)(void*)&data;
    AddToHash(d, sizeof(int), ref h);
}

public unsafe static void Hash(ref uint h, long data)
{ 
    byte* d= (byte*)(void*)&data;
    Hash(d, sizeof(long), ref h);
}

如果您只能在不了解内部的情况下访问该字段作为对象,则只需在每个字段上调用GetHashCode()并将其组合如下:

uint h = 0;
foreach(var item in collection) 
{
    Hash(ref h, item.GetHashCode());
}
return Avalanche(h);

可悲的是,你不能做sizeof(T)所以你必须单独完成每个结构。

如果你想使用反射,你可以在每个类型的基础上构建一个在所有字段上进行结构标识和散列的函数。

如果你想避免使用不安全的代码,那么你可以使用位掩码技术从int中提取单个位(如果处理字符串则为chars),而不会有太多额外的麻烦。

答案 2 :(得分:1)

这种方法没有任何问题,只要您组合的哈希码的成员遵循哈希码规则。简而言之......

  1. 私有成员的哈希码不应在对象的生命周期内更改
  2. 容器不得更改私有成员指向的对象,以免它反过来更改容器的哈希代码

答案 3 :(得分:1)

如果项目的顺序不重要(即{“a”,“b”}与{“b”,“a”})相同,那么您可以使用exclusive或组合哈希码:< / p>

hash ^= item.GetHashCode();

[编辑:正如马克在对不同答案的评论中指出的那样,这样做的缺点是也会给像{“a”}和{“a”,“b”,“b”}这样的集合提供相同的哈希码]

如果订单很重要,您可以乘以素数并添加:

hash *= 11;
hash += item.GetHashCode();

(当你乘以时,你有时会得到一个被忽略的溢出,但是乘以素数就会丢失最少的信息。如果你用16乘以一个数字,你每次会丢失4位信息因此,在八个项目之后,第一个项目的哈希码将完全消失。)