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

时间:2008-09-19 15:16:04

标签: c# hash internals

这个问题来自对tuples的讨论。

我开始考虑元组应该具有的哈希码。 如果我们接受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<object, object> dict = new Dictionary<object, object>();

            dict.Add(new KeyValuePair<MyClass,object>(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<MyClass,object>(new MyClass(1, 2), 1), 1); 

            return;
        }
    }
}

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

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

6 个答案:

答案 0 :(得分:14)

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

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

答案 1 :(得分:3)

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

有关此主题的更多信息(包括源代码)here

注意使用未经检查的关键字(以避免溢出)并在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<T1, T2, T3, T4>)) return false;
    return Equals((Quad<T1, T2, T3, T4>) obj);
}

public bool Equals(Quad<T1, T2, T3, T4> 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<T1, T2, T3, T4> left, Quad<T1, T2, T3, T4> right)
{
    return Equals(left, right);
}


public static bool operator !=(Quad<T1, T2, T3, T4> left, Quad<T1, T2, T3, T4> right)
{
    return !Equals(left, right);
}

答案 2 :(得分:2)

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

答案 3 :(得分:1)

现在看来我有一个线索。

我认为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<T, R>
        {
            public T First { get; set; }
            public R Second { get; set; }
        }

        static void Main(string[] args)
        {
            var dict = new Dictionary<Pair<int, MyClass>, object>();

            dict.Add(new Pair<int, MyClass>() { 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<int, MyClass>() { First = 1, Second = new MyClass(1, 3) }, 1);

            return;
        }
    }
}

答案 4 :(得分:0)

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

这完全取决于我简要阅读的内容,所以请按照你的意愿去做。

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

编辑:提供类似

的链接
  

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

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

答案 5 :(得分:-1)

  

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

您的示例似乎证明了其他方式:-) MyClass的密钥1和值KeyValuePair的哈希码是相同的。Key。 KeyValuePair实现必须同时使用其ValueObject.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; } /// <summary> /// Figure out if we are equal distance, converting into the same units of measurement if we have to /// </summary> /// <param name="obj">the other guy</param> /// <returns>true if we are the same distance</returns> 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<object, object> dict = new Dictionary<object, object>(); dict.Add(new KeyValuePair<MyDistanceClass, object>(distPoint, 1), 1); //this should not barf dict.Add(new KeyValuePair<MyDistanceClass, object>(distInch, 1), 1); return; } } ,而是调用由您提供的实例提供的GetHashCode()实现。

考虑一个更复杂的案例:

{{1}}

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