为什么ValueType.GetHashCode()实现得像?

时间:2010-10-01 17:28:18

标签: c# gethashcode

来自ValueType.cs

**Action: Our algorithm for returning the hashcode is a little bit complex. We look 
**        for the first non-static field and get it's hashcode.  If the type has no 
**        non-static fields, we return the hashcode of the type. We can't take the
**        hashcode of a static member because if that member is of the same type as 
**        the original type, we'll end up in an infinite loop.

今天当我使用KeyValuePair作为字典中的键(它存储了xml属性名称(枚举)和它的值(字符串))时,我被此感到厌烦,并期望它具有基于所有计算的哈希码它的领域,但根据实施它只考虑了关键部分。

示例(来自Linqpad的c / p):

void Main()
{
    var kvp1 = new KeyValuePair<string, string>("foo", "bar");
    var kvp2 = new KeyValuePair<string, string>("foo", "baz");

    // true
    (kvp1.GetHashCode() == kvp2.GetHashCode()).Dump();
}

第一个非静态字段我猜是指声明顺序中的第一个字段,这也可能因为任何原因在源中更改变量顺序而导致麻烦,并且认为它不会在语义上更改代码。

5 个答案:

答案 0 :(得分:43)

ValueType.GetHashCode()的实际实现与注释不完全匹配。它有两个版本的算法,快速和慢速。它首先检查结构是否包含引用类型的任何成员以及字段之间是否有任何填充。填充是结构值中的空白空间,在JIT编译器对齐字段时创建。在包含bool和int(3个字节)的结构中有填充,但是当它包含int和int时没有填充,它们紧密地结合在一起。

没有引用且没有填充,它可以执行快速版本,因为结构值中的每个位都是属于字段值的位。它一次只需4个字节。您将获得一个考虑所有成员的“好”哈希码。 .NET框架中的许多简单结构类型都以这种方式运行,如Point和Size。

如果没有通过测试,它会做慢速版本,相当于反思。这就是你得到的,你的KeyValuePair&lt;&gt;包含参考。而这个只检查第一个候选字段,就像评论所说的那样。这肯定是一个性能优化,避免燃烧太多时间。

是的,令人讨厌的细节而不是众所周知的。当有人注意到他们的收集代码糟透了时,通常会发现它。

另一个令人难以忍受的细节:当结构包含十进制类型的字段时,快速版本有一个字节错误。 12m和12.0m的值在逻辑上相等,但它们没有相同的位模式。 GetHashCode()会说它们不相等。哎哟。

答案 1 :(得分:31)

我没有实施它,我没有和那些做过的人交谈过。但我可以指出一些事情。

(在我继续之前,请注意,我在这里专门讨论用于平衡哈希表的哈希码,其中表的内容由非恶意用户选择。数字签名的哈希码问题,冗余当某些用户对表提供程序进行拒绝服务攻击时,检查或确保哈希表的良好性能超出了本讨论的范围。)

首先,正如Jon正确指出的那样,给定的算法确实实现了GetHashCode所需的契约。它可能不是您的目的,但它是合法的。所有必需是比较相等的东西具有相同的哈希码。

那么除了合同之外,还有什么“好东西”呢?一个好的哈希代码实现应该是:

1)快。非常快!请记住,首先哈希码的重点是快速在哈希表中找到一个相对空的槽。如果哈希码的O(1)计算实际上比天真地进行查找所花费的O(n)时间慢,则哈希码解决方案是净损失。

2)针对给定的输入分布,在32位整数空间内分布均匀。整个整体的分布越差,就越像哈希表的天真线性查找。

那么,在给定这两个冲突的目标的情况下,如何为任意值类型制作哈希算法?任何时候你花在一个复杂的哈希算法上,保证良好的分布花费的时间很少。

一个常见的建议是“哈希所有字段,然后将得到的哈希码与XOR一起”。但那是在乞求这个问题;当输入本身非常均匀且彼此不相关时,对两个32位整数进行异或运算只能提供良好的分布,这是不太可能的情况:

// (Updated example based on good comment!)
struct Control
{
    string name;
    int x;
    int y;
}

x和y在整个32位整数范围内均匀分布的可能性是多少?非常低。赔率要好得多,因为它们彼此接近,在这种情况下,将他们的哈希码合并在一起会使事情更糟,而不是< EM>更好。将彼此接近的整数放在一起将大部分比特归零。

此外,这是字段数量的O(n)!具有大量小字段的值类型将花费相对长的时间来计算哈希码。

基本上我们在这里的情况是用户自己没有提供哈希码实现;要么他们不关心,要么他们不希望这种类型被用作哈希表中的密钥。鉴于您没有关于类型的语言信息,最好的做法是什么?最好的事情是快速的,并且大多数时候都会产生良好的效果。

大多数情况下,两个不同的结构实例在大多数的字段中不同,而不仅仅是一个的字段,所以只需选择其中一个并希望这是不同的似乎是合理的。

大多数情况下,两个不同的结构实例在其字段中会有一些冗余,因此将多个字段的哈希值组合在一起可能会减少而不是增加哈希值中的熵,即使它消耗了哈希算法设计用于保存的时间。

将此与C#中的匿名类型设计进行比较。对于匿名类型,我们知道该类型很可能被用作表的键。我们知道很可能跨匿名类型的实例存在冗余(因为它们是笛卡尔积或其他连接的结果)。因此,我们将所有字段的哈希码组合成一个哈希码。如果由于计算的哈希代码数量过多而导致性能下降,则可以自由使用自定义名义类型而不是匿名类型。

答案 2 :(得分:7)

即使字段顺序发生变化,它仍然应遵守GetHashCode的约定:在该过程的生命周期内,相等的值将具有相同的哈希码。

特别是:

  • 非等值不必具有不相等的哈希码
  • 散列代码不必跨进程保持一致(您可以更改实现,重建,并且一切都应该仍然有效 - 基本上您不应该持久化哈希代码)

现在我并不是说ValueType的实现是一个好主意 - 它会以各种方式导致性能损失......但我不认为它实际上是已经破坏

答案 3 :(得分:3)

嗯,GetHashCode()的任何实现都有利弊。这些当然是我们在实施自己时所权衡的事情,但在ValueType.GetHashCode()的情况下,特别困难的是他们没有太多关于具体类型的实际细节的信息。当然,当我们创建一个抽象类或者一个旨在成为类的基础的类时,我们经常会遇到这种情况,这些类会在状态方面增加更多,但在这些情况下,我们只有使用默认实现的明显解决方案object.GetHashCode()除非派生类关心在那里覆盖它。

使用ValueType.GetHashCode()他们没有这种奢侈,因为值类型和引用类型之间的主要区别在于,尽管讨论堆栈与堆的实现细节很受欢迎,但对于值而言这一事实类型等价与值相关,而对象类型等价与身份相关(即使对象通过覆盖Equals()GetHashCode()来定义不同的等价形式,引用相等的概念仍然存在并且仍然有用

因此,对于Equals()方法,实现是显而易见的;检查两个对象是否是相同的类型,如果是,那么还检查所有字段是否相等(实际上有一个优化,在某些情况下进行逐位比较,但这是对同一基本想法的优化)。

如何处理GetHashCode()?根本没有完美的解决方案。他们可以做的一件事就是在每个领域都有某种多次加法或转移然后xor。这可能会给出一个非常好的哈希码,但是如果有很多字段可能会很昂贵(更不用说它不推荐使用具有大量字段的值类型,实现者必须考虑它们仍然可以,实际上甚至有时甚至可能有意义,尽管我真的无法想象它既有意义也有意义,并且它也是有意义的。)如果他们知道某些字段在实例之间很少不同,那么他们可以忽略这些字段并且仍然具有非常好的哈希码,同时也非常快。最后,他们可以忽略大多数领域,并希望他们不忽视的领域在大多数时候都有不同的价值。他们选择了后者的最极端版本。

(当没有实例字段时所做的事情是另一个问题和一个相当不错的选择,这样的值类型等于所有其他相同类型的实例,并且它们具有与之匹配的哈希码)。

所以,如果你在第一个字段相同的情况下散列很多值(或以其他方式返回相同的哈希码),这是一个很糟糕的实现,但在其他情况下其他实现会很糟糕(Mono用于xoring所有字段'哈希码一起,在你的情况下更好,在其他情况下更糟糕。)

更改字段顺序的问题无关紧要,因为哈希码非常清楚地表明只在进程的生命周期内保持有效,并且不适用于大多数情况下它们可以持续超出该范围(在某些情况下可能有用)如果在代码更改后没有找到正确的东西,那么它就不会受到影响。

所以,不是很好,但没有什么是完美的。它表明,在将对象用作关键字时,必须始终考虑“等式”意味着什么。您可以通过以下方式轻松修复:

public class KVPCmp<TKey, TValue> : IEqualityComparer<KeyValuePair<TKey, TValue>>, IEqualityComparer
{
  bool IEqualityComparer.Equals(object x, object y)
  {
      if(x == null)
        return y == null;
      if(y == null)
        return false;
      if(!(x is KeyValuePair<TKey, TValue>) || !(y is KeyValuePair<TKey, TValue>))
        throw new ArgumentException("Comparison of KeyValuePairs only.");
      return Equals((KeyValuePair<TKey, TValue>) x, (KeyValuePair<TKey, TValue>) y);
  }
  public bool Equals(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y)
  {
      return x.Key.Equals(y.Key) && x.Value.Equals(y.Value);
  }
  public int GetHashCode(KeyValuePair<TKey, TValue> obj)
  {
      int keyHash = obj.GetHashCode();
      return ((keyHash << 16) | (keyHash >> 16)) ^ obj.Value.GetHashCode();
  }
  public int GetHashCode(object obj)
  {
      if(obj == null)
        return 0;
      if(!(obj is KeyValuePair<TKey, TValue>))
       throw new ArgumentException();
      return GetHashCode((KeyValuePair<TKey, TValue>)obj);
  }
}

在创建字典时使用它作为比较器,一切都应该很好(你只需要通用的比较器方法,但其余部分并不会造成任何伤害,并且有时会有用)。

答案 4 :(得分:0)

谢谢大家非常非常有益的答案。我知道在这个决定中必须有一些理由,但我希望它有更好的记录。我无法使用框架的v4,因此没有Tuple<>,这是我决定搭载KeyValuePair结构的主要原因。但我想没有偷工减料,我必须自己动手。再一次,谢谢大家。