c#如何计算对象的哈希码?

这个问题来自关于元组的讨论。

我开始考虑元组应该具有的哈希码。 如果我们接受KeyValuePair类作为元组怎么办? 它不会覆盖GetHashCode()方法,所以可能它不会知道它的“子”的哈希码…所以,运行时将调用Object.GetHashCode(),它不知道真实的对象结构。

然后我们可以创建一些引用类型的实例,它们实际上是Equal,因为重载的GetHashCode()和Equals()。 并将它们用作元组中的“孩子”来“欺骗”字典。

但它不起作用! 运行时以某种方式计算出我们元组的结构并调用我们类的重载GetHashCode!

它是如何工作的? Object.GetHashCode()的分析是什么?

当我们使用一些复杂的密钥时,它会在某些不好的情况下影响性能吗? (可能,不可能的情况……但仍然)

以此代码为例:

namespace csharp_tricks { class Program { class MyClass { int keyValue; int someInfo; public MyClass(int key, int info) { keyValue = key; someInfo = info; } public override bool Equals(object obj) { MyClass other = obj as MyClass; if (other == null) return false; return keyValue.Equals(other.keyValue); } public override int GetHashCode() { return keyValue.GetHashCode(); } } static void Main(string[] args) { Dictionary dict = new Dictionary(); dict.Add(new KeyValuePair(new MyClass(1, 1), 1), 1); //here we get the exception -- an item with the same key was already added //but how did it figure out the hash code? dict.Add(new KeyValuePair(new MyClass(1, 2), 1), 1); return; } } } 

更新我想我已经在下面的答案中找到了解释。 它的主要成果是:

  • 小心你的密钥及其哈希码:-)
  • 对于复杂的字典键,您必须正确覆盖Equals()和GetHashCode()。

不要在可变类上覆盖GetHashcode()和Equals(),只在不可变类或结构上覆盖它,否则如果修改用作键的对象,哈希表将不再正常工作(你将无法修改密钥对象后检索与密钥关联的值)

哈希表也不使用哈希码来识别他们使用密钥对象自己作为标识符的对象,并不要求用于在哈希表中添加条目的所有密钥都返回不同的哈希码,但是建议他们这样做,否则性能受苦很大

以下是Quad元组的正确Hash和相等实现(内部包含4个元组组件)。 此代码确保在HashSet和字典中正确使用此特定元组。

这里有关于这个主题的更多信息(包括源代码)。

注意使用unchecked关键字(以避免溢出)并在obj为null时抛出NullReferenceException(根据基本方法的要求)

 public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) throw new NullReferenceException("obj is null"); if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != typeof (Quad)) return false; return Equals((Quad) obj); } public bool Equals(Quad obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; return Equals(obj.Item1, Item1) && Equals(obj.Item2, Item2) && Equals(obj.Item3, Item3) && Equals(obj.Item4, Item4); } public override int GetHashCode() { unchecked { int result = Item1.GetHashCode(); result = (result*397) ^ Item2.GetHashCode(); result = (result*397) ^ Item3.GetHashCode(); result = (result*397) ^ Item4.GetHashCode(); return result; } } public static bool operator ==(Quad left, Quad right) { return Equals(left, right); } public static bool operator !=(Quad left, Quad right) { return !Equals(left, right); } 

查看Brad Abrams的这篇文章以及Brian Grunkemeyer的评论,了解有关object.GetHashCode如何工作的更多信息。 另外,请看一下Ayande博客文章的第一条评论。 我不知道框架的当前版本是否仍然遵循这些规则,或者他们是否真的像Brad所暗示的那样改变了它。

好像我现在有了一个线索。

我认为KeyValuePair是一个引用类型,但它不是,它是一个结构。 因此它使用ValueType.GetHashCode()方法。 它的MSDN说:“派生类型的一个或多个字段用于计算返回值”。

如果您将真正的引用类型作为“元组提供者”,您将欺骗字典(或您自己……)。

 using System.Collections.Generic; namespace csharp_tricks { class Program { class MyClass { int keyValue; int someInfo; public MyClass(int key, int info) { keyValue = key; someInfo = info; } public override bool Equals(object obj) { MyClass other = obj as MyClass; if (other == null) return false; return keyValue.Equals(other.keyValue); } public override int GetHashCode() { return keyValue.GetHashCode(); } } class Pair { public T First { get; set; } public R Second { get; set; } } static void Main(string[] args) { var dict = new Dictionary, object>(); dict.Add(new Pair() { First = 1, Second = new MyClass(1, 2) }, 1); //this is a pair of the same values as previous! but... no exception this time... dict.Add(new Pair() { First = 1, Second = new MyClass(1, 3) }, 1); return; } } } 

我不再有书的参考了,我必须找到它只是为了确认,但我认为默认的基础哈希只是将对象的所有成员混合在一起。 由于CLR的工作方式,它可以访问它们,所以它不是你能写的东西。

这完全取决于我简要阅读的内容,因此请将其视为您的意愿。

编辑:这本书是来自MS Press的Inside C# 。 盖子上有锯片的那个。 作者花了很多时间解释如何在CLR中实现内容,语言如何转换为MSIL等。 等。 如果你能找到这本书,那就不错了。

编辑:形成提供的链接

Object.GetHashCode()使用System.Object类中的内部字段来生成哈希值。 创建时,为每个创建的对象分配一个唯一的对象键,存储为整数。 这些键从1开始,每次创建任何类型的新对象时都会递增。

嗯,我想我需要编写一些自己的哈希码,如果我希望将对象用作哈希键。

所以可能它不会知道它的“孩子”的哈希码。

您的示例似乎certificate不是:-) KeyCaluePair的密钥MyClass和值1的哈希码是相同的。 KeyValuePair实现必须为其自己的哈希代码使用其KeyValue

向上移动,字典类需要唯一键。 它使用每个键提供的哈希码来解决问题。 请记住,运行时不是调用Object.GetHashCode() ,而是调用由您提供的实例提供的GetHashCode()实现。

考虑一个更复杂的情况:

 public class HappyClass { enum TheUnit { Points, Picas, Inches } class MyDistanceClass { int distance; TheUnit units; public MyDistanceClass(int theDistance, TheUnit unit) { distance = theDistance; units = unit; } public static int ConvertDistance(int oldDistance, TheUnit oldUnit, TheUnit newUnit) { // insert real unit conversion code here :-) return oldDistance * 100; } ///  /// Figure out if we are equal distance, converting into the same units of measurement if we have to ///  /// the other guy /// true if we are the same distance public override bool Equals(object obj) { MyDistanceClass other = obj as MyDistanceClass; if (other == null) return false; if (other.units != this.units) { int newDistance = MyDistanceClass.ConvertDistance(other.distance, other.units, this.units); return distance.Equals(newDistance); } else { return distance.Equals(other.distance); } } public override int GetHashCode() { // even if the distance is equal in spite of the different units, the objects are not return distance.GetHashCode() * units.GetHashCode(); } } static void Main(string[] args) { // these are the same distance... 72 points = 1 inch MyDistanceClass distPoint = new MyDistanceClass(72, TheUnit.Points); MyDistanceClass distInch = new MyDistanceClass(1, TheUnit.Inch); Debug.Assert(distPoint.Equals(distInch), "these should be true!"); Debug.Assert(distPoint.GetHashCode() != distInch.GetHashCode(), "But yet they are fundimentally different values"); Dictionary dict = new Dictionary(); dict.Add(new KeyValuePair(distPoint, 1), 1); //this should not barf dict.Add(new KeyValuePair(distInch, 1), 1); return; } } 

基本上……在我的例子中,你想要两个相同距离的对象为Equals返回“true”,但是返回不同的哈希码。