【问题标题】:Is GetHashCode guaranteed to be the same for the lifetime of an object?GetHashCode 是否保证在对象的生命周期内相同?
【发布时间】:2019-05-02 20:58:12
【问题描述】:

我讨厌打败 dead horse。在@eric-lippert 的blog 中,他说:

对象的哈希值在其整个生命周期内都是相同的

然后跟进:

然而,这只是一个理想情况的指导方针

所以,我的问题是这样的。

对于 POCO(没有被覆盖)或框架(如 FileInfoProcess)对象,GetHashCode() 方法的返回值是否保证在其生命周期内保持相同?

附:我说的是预分配的对象。 var foo = new Bar(); foo.GetHashCode() 将始终返回相同的值。

【问题讨论】:

  • Is the return value of the GetHashCode() method guaranteed to be the same during its lifetime没有。易于测试,Console.WriteLine("What am I?".GetHashCode()) Console.WriteLine("What am I?".GetHashCode()) 运行这些,你会看到。 GetHashCode 从来都不是真正稳定的;一个原因是因为它在 .NET 中的不同实现中可能会有所不同
  • @Çöđěxěŕ 是一样的。
  • @Çöđěxěŕ 这就是为什么我说对象的生命周期。
  • Sh!tness,我很抱歉,是的,在同一程序执行中的生命周期,,但不是不同的程序执行。例如在控制台应用程序中运行它,然后在另一个应用程序中运行它,您应该会看到不同...
  • 如果你自己不重写 GetHashCode,你是安全的。如果您确实覆盖 GetHashCode 并遵守此约定,那么您是安全的。 .NET 框架/标准/核心类遵守此约定(我不知道框架库中的任何类型会违反此约定)。除非你使用来自完全不称职的程序员的第 3 方代码(请原谅),否则来自第 3 方库的类型也应该没有问题......

标签: c# .net-core


【解决方案1】:

如果您查看MSDN documentation,您会发现以下关于 GetHashCode 方法的默认行为的注释:

如果 GetHashCode 没有被覆盖,引用类型的哈希码是 通过调用基类的 Object.GetHashCode 方法计算, 它根据对象的引用计算哈希码;更多 信息,请参阅 RuntimeHelpers.GetHashCode。换句话说,两个 ReferenceEquals 方法返回 true 的对象具有 相同的哈希码。如果值类型不覆盖 GetHashCode,则 基类的 ValueType.GetHashCode 方法使用反射来 根据类型字段的值计算哈希码。在 换句话说,其字段具有相等值的值类型具有相等 哈希码

根据我的理解,我们可以假设:

  • 对于引用类型(不覆盖 Object.GetHashCode) 给定实例的哈希码值保证为 实例的整个生命周期都相同(因为内存 存储对象的地址在其期间不会改变 终生)
  • 对于一个值类型(它不会覆盖 Object.GetHashCode),它取决于:如果值类型是不可变的,那么哈希码不会 在其生命周期内发生变化。如果,否则,其字段的值 可以在创建后更改,然后其哈希码也会更改。 请注意,值类型通常是不可变的。

重要编辑

正如上面的一条评论所指出的,.NET 垃圾收集器可以决定在对象生命周期内移动对象在内存中的物理位置,换句话说,对象可以在托管内存中“重新定位”。

这是有道理的,因为垃圾收集器负责管理创建对象时分配的内存。

经过一番搜索并根据this stackoverflow question(阅读用户@supercat提供的cmets)似乎这种重定位不会改变对象实例在其生命周期内的哈希码,因为哈希码计算一次(第一次请求它的值),计算的值被保存并在以后重用(当 再次请求哈希码值)。

总而言之,根据我的理解,您唯一可以假设的是给定两个指向内存中同一对象的引用,它们的哈希码将始终相同。换句话说,如果 Object.ReferenceEquals(a, b) 则 a.GetHashCode() == b.GetHashCode()。此外,似乎给定一个对象实例,其哈希码在其整个生命周期内都将保持不变,即使该对象的物理内存地址已被垃圾收集器更改。

关于哈希码使用的旁注

请务必记住,在 .NET 框架中引入哈希码的唯一目的是处理哈希表数据结构。

为了确定给定value要使用的桶,取对应的key并计算其哈希码(准确地说,桶索引通过对 GetHashCode 调用返回的值应用一些规范化来获得,但细节对于本次讨论并不重要)。换句话说,.NET 实现哈希表时使用的哈希函数是基于对键的哈希码的计算。

这意味着哈希码的唯一安全用法是平衡哈希表,正如 Eric Lippert here 所指出的那样,因此不要编写依赖于哈希码值的代码用于任何其他目的。

【讨论】:

  • 这回答了我的问题。附加一个......你会如何分类stringstring s = "1"; Console.WriteLine(s.GetHashCode());s = "2"; Console.WriteLine(s.GetHashCode()); 产生 2 个不同的结果。
  • System.String 是一个引用类型。在您的示例中,您得到两个不同的哈希码,因为您引用了两个不同的字符串(从文字“1”获得的字符串和从“2”获得的字符串是内存中的两个不同对象)。
  • “因为存储对象的内存地址在其生命周期内不会改变” - 我认为垃圾收集器可以重新定位对象?
  • @EnricoMassone 我是这么认为的。感谢您的确认。
  • @AngryHacker,注意“1”和“2”是两个不同的对象实例(字符串实例)。没有这样的约定/规则规定两个不同的对象实例必须返回相等的哈希码(除非两个实例被认为是相等的,在这种情况下,每个实例的哈希值也必须相等)
【解决方案2】:

有三种情况。

  1. 一个不覆盖GetHashCode的类
  2. 一个不覆盖GetHashCode的结构体
  3. 覆盖GetHashCode 的类或结构

如果一个类没有覆盖GetHashCode,则使用辅助函数RuntimeHelpers.GetHashCode的返回值。这将在每次为同一个对象调用时返回相同的值,因此一个对象将始终具有相同的哈希码。请注意,此哈希码特定于单个 AppDomain - 重新启动您的应用程序或创建另一个 AppDomain 可能会导致您的对象获得不同的哈希码。

如果结构没有覆盖GetHashCode,则根据其中一个成员的哈希码生成哈希码。当然,如果您的结构是可变的,那么该成员可能会随着时间而改变,因此哈希码也会随着时间而改变。即使结构是不可变的,该成员本身也可能发生变异,并可能返回不同的哈希码。

如果一个类或结构确实覆盖了GetHashCode,那么所有的赌注都被取消了。有人可以通过返回一个随机数来实现GetHashCode——这有点傻,但完全有可能。更有可能的是,该对象可能是可变的,其哈希码可能基于其成员,这两者都可能随着时间而改变。

对于可变对象或哈希码可以随时间变化的对象(在给定的 AppDomain 中)实现 GetHashCode 通常是个坏主意。在这种情况下,Dictionary<TKey, TValue> 等类所做的许多假设都会失效,您可能会看到奇怪的行为。

【讨论】:

  • 当你说bad idea to implement GetHashCode for objects which are mutable时,你的意思是在你自己的类中重写该方法吗?
  • 是的。在可变对象中覆盖 GetHashCode 是个坏主意。你只能覆盖你自己类中的任何方法——你不能覆盖别人类中的方法
  • 拥有一个在对象的生命周期内更改的哈希码是个坏主意,因此为可变对象实现 GetHashCode 是个坏主意...除非您不考虑任何可变对象成员成为身份的一部分。但这确实适用性非常有限。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-04-27
  • 1970-01-01
  • 2020-02-26
  • 1970-01-01
  • 1970-01-01
  • 2014-12-24
相关资源
最近更新 更多