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

来自ValueType.cs

 **动作:我们返回哈希码的算法有点复杂。 我们看 
 **为第一个非静态字段并获取它的哈希码。 如果类型没有 
 **非静态字段,我们返回该类型的哈希码。 我们不能接受
 **静态成员的哈希码,因为如果该成员的类型与 
 **原始类型,我们将以无限循环结束。

今天,当我使用KeyValuePair作为字典中的键(它存储了xml属性名称(枚举)和它的值(字符串))时,我被它咬了,并期望它根据其所有字段计算它的哈希码,但根据实施情况,它只考虑了关键部分。

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

 void Main() { var kvp1 = new KeyValuePair("foo", "bar"); var kvp2 = new KeyValuePair("foo", "baz"); // true (kvp1.GetHashCode() == kvp2.GetHashCode()).Dump(); } 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

如果没有通过测试,那就是慢速版本,反思的道德等价物。 这就是你得到的,你的KeyValuePair <>包含引用。 而且这个只检查第一个候选字段,就像评论所说的那样。 这肯定是一个性能优化,避免燃烧太多时间。

是的,讨厌的细节而不是广为人知。 当有人注意到他们的收集代码吸收泥时,通常会发现它。

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

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

特别是:

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

现在我不是说ValueType的实现是一个好主意 – 它会以各种方式引起性能损失……但我认为它实际上并没有被破坏

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

使用ValueType.GetHashCode()它们没有这种奢侈,因为值类型和引用类型之间的主要区别在于,尽管讨论堆栈与堆的实现细节很受欢迎,但是对于值类型等价关系的事实为了对象类型,等价与身份相关(即使当一个对象通过重写Equals()GetHashCode()定义不同的等价forms时,引用相等的概念仍然存在并且仍然有用。

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

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

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

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

更改字段顺序的问题并不重要,因为哈希码非常明确地表示为仅在进程的生命周期内保持有效,并且不适用于大多数情况下它们可以持续超出该范围(在某些缓存情况下可能有用)如果在代码更改后找不到正确的东西,它不会受到伤害。

所以,不是很好,但没有什么是完美的。 它表明,在将对象用作关键字时,必须始终考虑“等式”的含义。 它可以很容易地修复你的情况:

 public class KVPCmp : IEqualityComparer>, IEqualityComparer { bool IEqualityComparer.Equals(object x, object y) { if(x == null) return y == null; if(y == null) return false; if(!(x is KeyValuePair) || !(y is KeyValuePair)) throw new ArgumentException("Comparison of KeyValuePairs only."); return Equals((KeyValuePair) x, (KeyValuePair) y); } public bool Equals(KeyValuePair x, KeyValuePair y) { return x.Key.Equals(y.Key) && x.Value.Equals(y.Value); } public int GetHashCode(KeyValuePair 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)) throw new ArgumentException(); return GetHashCode((KeyValuePair)obj); } } 

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

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