Object.GetHashCode ()的默认实现

GetHashCode()的默认实现是如何工作的? 它是否有效且足够好地处理结构、类、数组等?

我试图决定在哪些情况下我应该自己打包,在哪些情况下我可以安全地依靠默认实现来做好。如果可能的话,我不想重新发明轮子。

69184 次浏览

一般来说,如果要重写 Equals,则需要重写 GetHashCode。这是因为两者都用于比较类/结构的相等性。

检查时使用等于 Foo A,B;

如果(A = = B)

因为我们知道指针不太可能匹配,所以我们可以比较内部成员。

Equals(obj o)
{
if (o == null) return false;
MyType Foo = o as MyType;
if (Foo == null) return false;
if (Foo.Prop1 != this.Prop1) return false;


return Foo.Prop2 == this.Prop2;
}

哈希表通常使用 GetHashCode。对于类给定的状态,类生成的散列码应该始终相同。

我通常会这么做,

GetHashCode()
{
int HashCode = this.GetType().ToString().GetHashCode();
HashCode ^= this.Prop1.GetHashCode();
etc.


return HashCode;
}

有些人会说散列码应该在每个对象生命周期中只计算一次,但是我不同意这种说法(我可能错了)。

使用对象提供的默认实现,除非对某个类具有相同的引用,否则它们将不相等。通过重写 Equals 和 GetHashCode,可以基于内部值而不是对象引用报告相等性。

namespace System {
public class Object {
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern int InternalGetHashCode(object obj);


public virtual int GetHashCode() {
return InternalGetHashCode(this);
}
}
}

InternalGetHashCode 被映射到 CLR 中的 ObjectNative: : GetHashCode函数,如下所示:

FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {
CONTRACTL
{
THROWS;
DISABLED(GC_NOTRIGGER);
INJECT_FAULT(FCThrow(kOutOfMemoryException););
MODE_COOPERATIVE;
SO_TOLERANT;
}
CONTRACTL_END;


VALIDATEOBJECTREF(obj);


DWORD idx = 0;


if (obj == 0)
return 0;


OBJECTREF objRef(obj);


HELPER_METHOD_FRAME_BEGIN_RET_1(objRef);        // Set up a frame


idx = GetHashCodeEx(OBJECTREFToObject(objRef));


HELPER_METHOD_FRAME_END();


return idx;
}
FCIMPLEND

快递的完整实现相当大,所以只链接到 C + + 源代码更容易。

反对GetHashCode方法的文档说的是 “此方法的默认实现不得用作散列目的的唯一对象标识符。”,而 ValueType的方法说的是 ”如果调用派生类型的 GetHashCode 方法,则返回值不太可能适合用作哈希表中的键

byteshortintlongcharstring等基本数据类型实现了很好的 GetHashCode 方法。其他一些类和结构,例如 Point,实现了一个 GetHashCode方法,这个方法可能适合也可能不适合您的特定需求。你只需要尝试一下,看看它是否足够好。

每个类或结构的文档可以告诉您它是否覆盖默认实现。如果它没有覆盖它,你应该使用你自己的实现。对于在需要使用 GetHashCode方法的地方自己创建的任何类或结构,应该自己创建使用适当成员计算哈希代码的实现。

对于类来说,默认值本质上是引用相等性,这通常没有问题。如果编写 struct,则更常见的做法是覆盖相等性(尤其是避免装箱) ,但无论如何都很少编写 struct!

当重写相等时,应该始终有一个匹配的 Equals()GetHashCode()(例如,对于两个值,如果 Equals()返回 true,它们的 必须的返回相同的散列码,但反过来是 没有所需的)-通常也提供 ==/!= 操作符,并且经常实现 IEquatable<T>


现在,在生成散列时,HashCode实用程序类型非常有用; 例如:

return HashCode.Combine(field1, field2); // multiple overloads available here

如果没有的话:

为了生成哈希代码,通常使用因数和,因为这样可以避免对配对值的冲突——例如,对于一个基本的2字段哈希:

unchecked // disable overflow, for the unlikely possibility that you
{         // are compiling with overflow-checking enabled
int hash = 27;
hash = (13 * hash) + field1.GetHashCode();
hash = (13 * hash) + field2.GetHashCode();
return hash;
}

这样做的好处是:

  • {1,2}的哈希值与{2,1}的哈希值不同
  • {1,1}的哈希值与{2,2}的哈希值不同

如果只使用一个未加权的和或 xor (^)等,这可能是常见的。

因为我找不到一个解释 为什么的答案,我们应该为定制结构重写 GetHashCodeEquals,而 为什么的默认实现“不太可能适合用作哈希表中的键”,我将留下一个到 这篇博文的链接,这解释了为什么会发生一个问题的真实案例。

我建议阅读整篇文章,但这里有一个总结(强调和澄清补充)。

结构的默认哈希值缓慢且不是很好的原因:

按照 CLR 的设计方式,每次对 System.ValueTypeSystem.Enum类型中定义的成员的调用都可能导致 装箱分配[ ... ]

散列函数的实现者面临着一个两难困境: 要么做好散列函数的分布,要么做得快。在某些情况下,可以同时实现这两个目标,但是在 ValueType.GetHashCode中是 通常很难做到这一点

Struct 的规范散列函数“组合”所有字段的散列代码。但是在 ValueType方法中获得字段哈希代码的唯一方法是使用 使用反射。因此,CLR 作者决定将速度置于发行版和默认的 GetHashCode版本 只是返回第一个非空字段的哈希代码之上,并使用类型 id [ ... ]“ munges”它,这是一种合理的行为,除非它不是。例如,如果运气不好,并且结构的第一个字段对于大多数实例具有相同的值,那么散列函数将提供相同的结果一直都是。而且,正如您可能想象的那样,如果这些实例存储在哈希集或哈希表中,那么这将导致严重的性能影响。

非常慢。

[ ... ] ValueType.EqualsValueType.GetHashCode都有一个特殊的优化。如果一个类型没有“指针”,并且正确地打包了[ ... ] ,那么使用更优化的版本: GetHashCode迭代一个实例,XOR 块为4字节,Equals方法使用 memcmp比较两个实例。但是优化是非常棘手的。首先,很难知道什么时候启用了优化[ ... ]其次,内存比较不一定会给你正确的结果。下面是一个简单的例子: [ ... ] -0.0+0.0是相等的,但有不同的二进制表示。

文章中描述的现实问题:

private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
// Empty almost all the time
public string OptionalDescription { get; }
public string Path { get; }
public int Position { get; }
}

我们使用了一个元组,它包含一个具有默认相等实现的自定义结构。还有 不幸的是,struct 有一个可选的第一个字段,它几乎总是等于[空字符串]。性能还可以,直到集合中的元素数量显著增加,导致真正的性能问题,需要花费几分钟的时间来初始化一个包含成千上万个项目的集合。

因此,要回答“在什么情况下我应该自己打包,在什么情况下我可以安全地依赖于默认实现”这个问题,至少在 结构的情况下,只要您的自定义结构可能被用作哈希表或 Dictionary中的键,就应该覆盖 EqualsGetHashCode
我还建议在这种情况下实现 IEquatable<T>,以避免装箱。

正如其他答案所说,如果你正在编写一个 同学们,使用引用相等的默认哈希通常是好的,所以我不会在这种情况下打扰,除非你需要覆盖 Equals(然后你必须覆盖相应的 GetHashCode)。

如果你只是和 POCO 打交道,你可以使用这个工具来简化你的生活:

var hash = HashCodeUtil.GetHashCode(
poco.Field1,
poco.Field2,
...,
poco.FieldN);

...

public static class HashCodeUtil
{
public static int GetHashCode(params object[] objects)
{
int hash = 13;


foreach (var obj in objects)
{
hash = (hash * 7) + (!ReferenceEquals(null, obj) ? obj.GetHashCode() : 0);
}


return hash;
}
}

到目前为止,对象的默认 GetHashCode 实现与对象本身无关,并且对于每个对象应该是唯一的。这是密码:

    inline DWORD GetNewHashCode()
{
LIMITED_METHOD_CONTRACT;
// 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 = GetThreadId()*4 + 5;
m_dwHashCodeSeed = m_dwHashCodeSeed*multiplier + 1;
return m_dwHashCodeSeed;
}

这是调用堆栈:

Thread: : GetNewHashCode

对象: : ComputeHashCode

目标: : GetHashCodeEx