【问题标题】:Object.GetHashCodeObject.GetHashCode
【发布时间】:2009-07-16 19:28:09
【问题描述】:

我的问题可能与Default implementation for Object.GetHashCode() 重复,但我再次询问,因为我不理解该问题的公认答案。

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

“但是,因为这个索引在垃圾回收时对象被回收后可以重复使用,所以有可能为两个不同的对象获取相同的哈希码。”

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

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

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

“这个实现对于散列并不是特别有用;因此,派生类应该覆盖 GetHashCode”

这是真的吗?如果它不适合散列,那么如果它有什么好处呢,为什么还要定义为 Object 的方法?


我的最后一个(也许对我来说最重要的)问题是,如果我必须为具有“引用相等”语义的任意类型发明/覆盖 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_allocated 数据成员的大小更大)。

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

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

【问题讨论】:

  • 为了线程安全,可能使用 volatile 声明和互锁增量器。
  • 如果由于它不是线程安全的而偶尔发生意外碰撞,这有什么关系吗?我认为 GetHashCode 的实现不需要保证不同对象的唯一返回值,相反,只要它们大部分是唯一的就足够了。
  • 非常有趣的是,您的简单实现显着提高了查找性能,但代价是 4B/实例。一个有趣的权衡。

标签: c# .net hash gethashcode


【解决方案1】:

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

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

如果你有一个类的实例通过引用相等性进行比较,那么你不需要重写 GetHashCode;默认实现保证相同引用的两个对象具有相同的哈希码。 (你在同一个对象上调用了同一个方法两次,所以结果当然是一样的。)

如果您编写了一个实现自己的相等性的类,它不同于引用相等性,那么您需要重写 GetHashCode,以便比较为相等的两个对象具有相等的哈希码。

现在,您只需每次都返回零即可。那将是一个糟糕的哈希函数,但它是合法的。

好的散列函数的其他属性是:

  • GetHashCode 绝不应抛出异常

  • 比较可变状态的相等性并因此对其可变状态散列的可变对象很容易出错。您可以将对象放入哈希表中,对其进行变异,并且无法再次将其取出。尽量不要散列或比较可变状态的相等性。

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

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

【讨论】:

  • 将其解读为“默认 object.GetHashCode 是 GetHashCode 的一个很好的实现,适用于使用引用相等的类型,即类和/或接口(但不是结构),您没有覆盖 object.Equals;其中“良好的实现”意味着在将这些类型用作字典的键时具有良好/快速的性能。”
  • 是的,我们为您提供的GetHashCode的默认实现是相当不错的。
  • “一个好的哈希算法的目的是提高查找的性能”。仅此一项就可能是 OP 可以获得的最佳答案。
【解决方案2】:

问题:

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

如果默认生成GetHashCode实现,两个对象可能共享相同的哈希码,因为:

  1. 在对象的生命周期内不应更改默认的 GetHashCode 结果,默认实现可确保这一点。如果它可以更改,则 Hashtable 之类的类型无法处理此实现。那是因为预计默认哈希码是唯一实例标识符的哈希码(即使没有这样的标识符:))。
  2. GetHashCode 取值范围为整数 (2^32) 范围。

结论: 分配 2^32 个强引用对象(在 Win64 上必须很容易)就足够了。

最后在object.GetHashCode reference in MSDN中明确声明:GetHashCode方法的默认实现不保证不同对象的返回值唯一。此外,.NET Framework 不保证 GetHashCode 方法的默认实现,它返回的值在不同版本的 .NET Framework 之间是相同的。因此,此方法的默认实现不得用作散列目的的唯一对象标识符。

【讨论】:

    【解决方案3】:

    您实际上不需要修改只需要 reference 相等性的类上的任何内容。

    此外,从形式上讲,这不是一个好的实现,因为它的分布很差。散列函数应该具有合理的分布,因为它可以改善散列桶分布,并间接提高使用散列表的集合的性能。正如我所说,这是一个正式的答案,是设计散列函数时的准则之一。

    【讨论】:

    • 发行版有什么不好的地方?如果要将哈希映射到存储桶,您将哈希除以存储桶的数量并使用余数,那么在我看来,我的实现会将对象平均/均匀地分布在所有存储桶上。
    • 如果这是哈希桶映射算法,那将是正确的。例如,请参阅concentric.net/~Ttwang/tech/inthash.htm。 Quote: "对于一个hash函数来说,分布应该是均匀的。这意味着当hash结果用于计算hash桶地址时,所有桶被选中的可能性是相同的。另外,相似的hash key应该被hash到非常不同的哈希结果。理想情况下,哈希键中的单个位更改应该会影响哈希结果的所有位。"
    猜你喜欢
    • 1970-01-01
    • 2023-03-10
    • 1970-01-01
    • 2011-07-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-08
    相关资源
    最近更新 更多