Object.GetHashCode()的实现

15

我正在阅读 Effective C#,其中有一条关于 Object.GetHashCode() 的评论我没有理解:

Object.GetHashCode() 使用 System.Object 类中的一个内部字段生成哈希值。每个创建的对象都被分配一个唯一的对象键,以整数形式存储,当它被创建时。
这些键从1开始,并且每次创建任何类型的新对象时都会递增。对象标识字段在 System.Object 构造函数中设置,之后无法修改。Object.GetHashCode() 返回这个值作为给定对象的哈希码。

我尝试查看 Object.GetHashCode() 的文档,但没有找到任何相关信息。

我编写了简单的代码来打印新生成对象的哈希码:

using System;

namespace TestGetHashCode
{
    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 100; i++)
            {
                object o = new object();
                Console.WriteLine(o.GetHashCode());
            }
        }
    }
}

最先打印出来的几个数字是:

37121646,
45592480,
57352375,
2637164,
41014879,
3888474,
25209742,
26966483,
31884011

似乎与此不符。

这些键从1开始,每次创建任何类型的新对象时递增... Object.GetHashCode() 返回此值

然后,为了找到这个“System.Object中的内部字段”,我尝试使用ReSharper反编译源代码,但我找到的代码是:

[TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
[__DynamicallyInvokable]
public virtual int GetHashCode()
{
  return RuntimeHelpers.GetHashCode(this);
}

并且再次使用反编译的源代码,我发现RuntimeHelpers.GetHashCode被实现为

[SecuritySafeCritical]
[__DynamicallyInvokable]
[MethodImpl(MethodImplOptions.InternalCall)]
public static int GetHashCode(object o);

根据 MethodImpl 属性,似乎我无法查看实现,这对我来说是个死胡同。

请问有人能解释一下作者的评论吗?(第一个引用)

Object 类中的 internal 字段是什么,它如何用于实现 Object.GetHashCode()


1
@Yuval Itzchakov:你为什么认为GetHashCode()必须返回一个唯一标识符?它被称为Get*Hash*Code()而不是Get*UniqueIdentifier*(),目的是什么。 - zerkms
@Yuval Itzchakov:“这不是你完成这样的任务的方式”---这正是你要做的。对于一个没有任何可变成员的类型,你别无选择。因此,如果你愿意,甚至可以将其实现为return 42;,一切都会正常工作(虽然不够高效,但不会出错)。 - zerkms
你说得对。出于某种原因,我忽略了这是“对象”的事实。尽管将其实现为“return 42”将为所有返回的对象生成相等性。不确定你的意思是什么。 - Yuval Itzchakov
1
@Yuval Itzchakov:“将为所有返回的对象生成相等性”---这是不正确的。如果它们的哈希码匹配,您永远不应该将对象视为相等。实际上,正好相反:相等的对象必须返回相等的哈希码,但相等的哈希码并不意味着对象相等。http://msdn.microsoft.com/en-us/library/system.object.gethashcode(v=vs.110).aspx“您不应假设相等的哈希码意味着对象相等。” - zerkms
1
@Yuval Itzchakov:“只看哈希码相等的人”---没有人应该这样做。如果有人做了愚蠢的事情-这不是鼓励他们继续这样做的理由。对于.NET来说并非如此。如果是第三方库的情况-必须报告为错误并进行修复。 - zerkms
显示剩余6条评论
1个回答

21

好的,我最好写下来。这本书非常不准确。Object.GetHashCode()方法的值是在CLR内部生成的,并且仅在第一次调用GetHashCode()方法时计算。我将引用来自SSCLI20分发版的代码,clr/src/vm/thread.h有一个函数可以生成该数字,它看起来像这样(为了可读性进行了编辑):

inline DWORD GetNewHashCode()
{
    // Every thread has its own generator for hash codes so that we won't get into a 
    // situation where two threads consistently give out the same hash codes.
    // Choice of multiplier guarantees period of 2**32
    // see Knuth Vol 2 p16 (3.2.1.2 Theorem A).
    DWORD multiplier = m_ThreadId*4 + 5;
    m_dwHashCodeSeed = m_dwHashCodeSeed*multiplier + 1;
    return m_dwHashCodeSeed;
}

接下来,它将存储在对象的所谓同步块中,因此后续调用将返回相同的值。生成的32位中只有26位实际存储,同步块需要一些状态位的空间。仍然足以生成非常高质量的哈希码,冲突非常少。

代码中m_ThreadId变量的存在需要解释一下。随机数生成器种子为每个单独的线程存储。这是避免锁定的一个技巧。

m_dwHashCodeSeed在Thread构造函数中像这样初始化:

   // Initialize this variable to a very different start value for each thread
   // Using linear congruential generator from Knuth Vol. 2, p. 102, line 24
   dwHashCodeSeed = dwHashCodeSeed * 1566083941 + 1;
   m_dwHashCodeSeed = dwHashCodeSeed;

使用:

   static  DWORD dwHashCodeSeed = 123456789;

感谢你的回答。从这段代码中看来,GetNewHashCode 是一个仅依赖于 m_ThreadId 的函数,因此对于同一线程,在每次调用时都会生成相同的哈希码。我在这里错过了什么吗? - Belgi
不,它取决于每次生成新哈希码时更新的m_dwHashCodeSeed。每个线程都从不同的种子开始,底部片段。m_ThreadId只是增加了额外的随机性,Donald Knuth帮助实现了这一点。不要过分关注线程与此相关,这只是一种无锁代码技巧。锁是昂贵的。两个线程同时调用GetHashCode涉及一些诡计,我将其省略以保持其半可理解性。 - Hans Passant
1
我理解这个的工作原理并感谢您的写作,但是为什么它会像这样工作呢?像他们所做的那样“希望”没有冲突是不必要的,对吧?为什么不为对象生成一个哈希值,该哈希值基于该对象的唯一标识符?或者,如果不存在,则使用每次递增1的UID,并将其存储在SYNC块中?这与上述解决方案相同,只有可能发生绝对最小程度的冲突(仅当UID溢出时才会发生冲突)。 - Slight

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接