代码之家  ›  专栏  ›  技术社区  ›  ChrisW

对象.gethashcode

  •  18
  • ChrisW  · 技术社区  · 15 年前

    我的问题可能重复 Default implementation for Object.GetHashCode() 但我又问了一次,因为我不理解那个被接受的答案。

    首先,我有三个关于 accepted answer to the previous question 引文 some documentation 如下:

    但是,由于在垃圾回收过程中回收对象后可以重用此索引,因此可以为两个不同的对象获取相同的哈希代码。

    这是真的吗?在我看来,两个对象不会有相同的哈希代码,因为对象的代码在被垃圾收集(即不再存在)之前不会被重用。

    另外,表示相同值的两个对象只有在它们是完全相同的对象时才具有相同的哈希代码。

    这是个问题吗?例如,我想将一些数据与DOM树中的每个节点实例相关联。要做到这一点,“节点”必须有一个标识或哈希代码,以便我可以将它们用作数据字典中的键。散列码是否标识了它是否是“完全相同的对象”,即“引用相等而不是值相等”,我想要什么?

    此实现对哈希处理不是特别有用;因此,派生类应重写GetHashCode。

    这是真的吗?如果它不适合散列,那么如果它有什么用处呢,为什么它甚至被定义为对象方法呢?


    最后一个(也许对我来说最重要)问题是,如果我必须为具有“引用相等”语义的任意类型发明/重写getHashCode()实现,那么下面是一个合理且良好的实现:

    class SomeType
    {
      //create a new value for each instance
      static int s_allocated = 0;
      //value associated with this instance
      int m_allocated;
      //more instance data
      ... plus other data members ...
      //constructor
      SomeType()
      {
        allocated = ++s_allocated;
      }
      //override GetHashCode
      public override int GetHashCode()
      {
        return m_allocated;
      }
    }
    

    编辑

    仅供参考,我使用以下代码对其进行了测试:

        class TestGetHash
        {
            //default implementation
            class First
            {
                int m_x;
            }
            //my implementation
            class Second
            {
                static int s_allocated = 0;
                int m_allocated;
                int m_x;
                public Second()
                {
                    m_allocated = ++s_allocated;
                }
                public override int GetHashCode()
                {
                    return m_allocated;
                }
            }
            //stupid worst-case implementation
            class Third
            {
                int m_x;
                public override int GetHashCode()
                {
                    return 0;
                }
            }
    
            internal static void test()
            {
                testT<First>(100, 1000);
                testT<First>(1000, 100);
                testT<Second>(100, 1000);
                testT<Second>(1000, 100);
                testT<Third>(100, 100);
                testT<Third>(1000, 10);
            }
    
            static void testT<T>(int objects, int iterations)
                where T : new()
            {
                System.Diagnostics.Stopwatch stopWatch =
                    System.Diagnostics.Stopwatch.StartNew();
                for (int i = 0; i < iterations; ++i)
                {
                    Dictionary<T, object> dictionary = new Dictionary<T, object>();
                    for (int j = 0; j < objects; ++j)
                    {
                        T t = new T();
                        dictionary.Add(t, null);
                    }
                    for (int k = 0; k < 100; ++k)
                    {
                        foreach (T t in dictionary.Keys)
                        {
                            object o = dictionary[t];
                        }
                    }
                }
                stopWatch.Stop();
                string stopwatchMessage = string.Format(
                    "Stopwatch: {0} type, {1} objects, {2} iterations, {3} msec",
                    typeof(T).Name, objects, iterations,
                    stopWatch.ElapsedMilliseconds);
                System.Console.WriteLine(stopwatchMessage);
            }
        }
    

    在我的机器上,结果/输出如下:

    First type, 100 objects, 1000 iterations, 2072 msec
    First type, 1000 objects, 100 iterations, 2098 msec
    Second type, 100 objects, 1000 iterations, 1300 msec
    Second type, 1000 objects, 100 iterations, 1319 msec
    Third type, 100 objects, 100 iterations, 1487 msec
    Third type, 1000 objects, 10 iterations, 13754 msec
    

    我的实现占用了默认实现的一半时间(但我的类型比我分配的m_数据成员的大小更大)。

    我的实现和默认实现都是线性扩展的。

    相比之下,作为一个健全性检查,愚蠢的实现开始时很糟糕,而且扩展性更差。

    3 回复  |  直到 15 年前
        1
  •  36
  •   Eric Lippert    15 年前

    哈希代码实现必须具有的最重要属性是:

    如果两个对象比较为相等,则它们必须具有相同的哈希代码。

    如果有一个类,其中类的实例按引用比较相等,则不需要重写GetHashCode;默认实现保证同一引用的两个对象具有相同的哈希代码。(您在同一对象上两次调用同一个方法,因此结果当然是相同的。)

    如果编写的类实现了与引用相等不同的相等性,则需要重写GetHashCode,以便比较为相等的两个对象具有相等的哈希代码。

    现在,您可以通过每次返回零来实现这一点。这将是一个糟糕的哈希函数,但它是合法的。

    好的哈希函数的其他属性是:

    • GetHashCode不应引发异常

    • 可变对象比较其可变状态的相等性,因此散列其可变状态,是危险的bug多发对象。您可以将对象放入哈希表中,对其进行变异,然后再也无法将其取出。尽量不要散列或比较可变状态下的相等性。

    • gethashcode应该非常快——记住,一个好的哈希算法的目的是提高查找的性能。如果散列速度慢,则无法快速进行查找。

    • 不相等比较的对象应具有不同的哈希代码,分布在32位整数的整个范围内。

        2
  •  2
  •   Alex Yakunin    15 年前

    问题:

    这是真的吗?在我看来,两个对象没有相同的哈希代码,因为 对象的代码在被垃圾收集(即不再存在)之前不会被重用。

    如果通过默认的gethashcode实现生成,则两个对象可以共享相同的哈希代码,因为:

    1. 在对象的生存期内,不应更改默认的GetHashCode结果 默认实现确保了这一点。如果它可以改变,诸如hashtable之类的类型就不能处理这个实现。这是因为期望默认哈希代码是唯一实例标识符的哈希代码(即使没有这样的标识符:)。
    2. GetHashCode值的范围是整数(2^32)的范围。

    结论: 只需将2^32个强引用对象分配给(在Win64上必须很容易)即可达到限制。

    最后,在 object.GetHashCode reference in MSDN :GetHashCode方法的默认实现不保证不同对象的唯一返回值。此外,.NET框架不保证GetHashCode方法的默认实现,它返回的值在不同版本的.NET框架之间是相同的。因此,不能将此方法的默认实现用作哈希目的的唯一对象标识符。

        3
  •  0
  •   Fabrício Matté    15 年前

    您实际上不需要修改类上只需要 参考 平等。

    而且,从形式上讲,这不是一个好的实现,因为它的分布很差。哈希函数应该有一个合理的分布,因为它改善了使用哈希表的集合中的哈希桶分布以及间接的性能。如我所说,这是一个 正式的 答:设计哈希函数时的一个指导原则。