Object.GetHashCode()的默认实现


162

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

我正在尝试确定在什么情况下应该打包自己的产品,以及在什么情况下我可以安全地依靠默认实现来做得很好。如果有可能,我不想重新发明轮子。




34
另外:您可以使用GetHashCode()System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj)
Marc Gravell

@MarcGravell感谢您的贡献,我正在寻找确切的答案。
Andrew Savinykh

@MarcGravell但是我将如何使用其他方法呢?
托马什Zato -恢复莫妮卡

Answers:


86
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

GetHashCodeEx的完整实现相当大,因此仅链接到C ++源代码就更容易了。


5
该文档引用必须来自非常早期的版本。在当前的MSDN文章中,它不再像这样编写,可能是因为它是完全错误的。
汉斯·帕桑

4
他们更改了措词,是的,但是仍然说了同样的话:“因此,不得将此方法的默认实现用作哈希目的的唯一对象标识符。”
大卫·布朗

7
为什么文档声称实现对哈希不是特别有用?如果一个对象等于其自身而没有其他任何东西,那么任何哈希码方法对于给定的对象实例将始终返回相同的值,并且对于不同的实例通常将返回不同的值,这是什么问题?
2013年

3
@ ta.speot.is:如果要确定某个实例是否已经添加到字典中,则引用相等是完美的。如您所注意到的,对于字符串,通常更感兴趣的是是否已经添加了包含相同字符序列的字符串。这就是为什么string覆盖GetHashCode。另一方面,假设您想统计各种控件处理Paint事件的次数。您可以使用Dictionary<Object, int[]>(每个int[]存储的内容只能容纳一个项目)。
2013年

6
@ It'sNotALie。然后感谢Archive.org提供的副本;-)
RobIII,2013年

88

对于一个类,默认值实质上是引用相等,通常很好。如果编写一个结构,则覆盖相等性更为常见(尤其是避免装箱),但是无论如何您都很难编写一个结构!

当重写等式,你应该始终有一个匹配的Equals()GetHashCode()(即两个值,如果Equals()返回true,他们必须返回相同的哈希码,但反过来不是必需的) -这是常见的也提供==/ !=运营商,并经常到实施IEquatable<T>

为了生成哈希码,通常使用分解和,因为这样可以避免在成对的值上发生冲突-例如,对于基本的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(^)等,则可能很常见。


关于因数和算法的好处的精彩观点;我以前没有意识到的事情!
漏洞

分解总和(如上所述)是否会偶尔导致溢出异常?
sinelaw 2013年

4
@sinelaw是的,应该执行unchecked。幸运的是,它unchecked是C#中的默认值,但最好将其显式表示。编辑
马克·格雷韦尔

7

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

基本数据类型,例如byteshortintlongchar以及string实现良好的GetHashCode方法。Point例如,其他一些类和结构实现的GetHashCode方法可能适合也可能不适合您的特定需求。您只需要尝试一下,看看它是否足够好。

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


2
我不同意您应该定期添加自己的实现。简而言之,绝不会对绝大多数类(尤其是类)进行相等性测试-或在其中存在的情况下,内置引用相等性很好。在(很罕见的)情况下,编写结构会更常见,更真实。
Marc Gravell

@马克·格雷夫(Marc Gravel):那当然不是我想说的。我将调整最后一段。:)
Guffa

至少在我看来,基本数据类型不能实现良好的GetHashCode方法。例如,的GetHashCode对于int返回数字本身:(123).GetHashCode()返回123
fdermishin

5
@ user502144那怎么了?这是一个易于计算的完美唯一标识符,不会对平等产生任何误报...
Richard Rast

@Richard Rast:可以,但是在哈希表中使用时,键可能会分配不正确。看看这个答案:stackoverflow.com/a/1388329/502144
fdermishin

5

由于我找不到答案来解释为什么我们应该覆盖GetHashCodeEquals定制结构,以及为什么默认实现“不太可能适合用作哈希表中的键”,因此,我将保留此博客的链接post,以真实的案例说明了所发生问题的原因。

我建议阅读整个文章,但这是一个摘要(添加了重点和说明)。

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

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

哈希函数的实现者面临两难选择:合理分配哈希函数或使其快速发展。在某些情况下,可以同时实现它们,但是很难在中通用实现ValueType.GetHashCode

结构的规范哈希函数“组合”所有字段的哈希码。但是在方法中获取字段的哈希码的唯一ValueType方法是使用反射。因此,CLR作者决定在分布上进行交易,默认GetHashCode版本仅返回第一个非空字段的哈希码,并使用类型id [修改]它[...]这是合理的行为,除非它不是。例如,如果您不够幸运,并且结构的第一个字段在大多数实例中具有相同的值,则哈希函数将始终提供相同的结果。而且,正如您可能想象的那样,如果将这些实例存储在哈希集或哈希表中,将会对性能产生巨大影响。

[...] 基于反射的实现速度很慢。非常慢。

[...]两者ValueType.EqualsValueType.GetHashCode有专门的优化。如果类型不具有“指针”并且已正确打包,则使用更佳的版本:GetHashCode对一个实例进行迭代,并对4个字节的XOR块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; }
}

我们使用了一个包含具有默认相等实现的自定义结构的元组。而且不幸的是,该结构具有一个可选的第一字段,该字段几乎总是等于[empty string]。性能良好,直到集合中的元素数量显着增加,导致真正的性能问题为止,花几分钟来初始化包含数万个项目的集合。

因此,要回答“在什么情况下我应该打包我自己的包以及在什么情况下我可以安全地依赖默认实现”的问题,至少在structs的情况下,您应该重写Equals并且GetHashCode每当将自定义结构用作键入哈希表或Dictionary
我也建议IEquatable<T>在这种情况下实施,以避免装箱。

就像其他答案所说的那样,如果您正在编写一个,则使用引用相等性的默认哈希值通常是可以的,因此在这种情况下我不会打扰,除非您需要重写Equals(然后您必须相应地重写GetHashCode)。


1

一般来说,如果要覆盖Equals,则要覆盖GetHashCode。这样做的原因是因为两者都用于比较类/结构的相等性。

在检查Foo A,B时使用Equals。

如果(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;
}

有人会说哈希码每个对象生命周期仅应计算一次,但是我不同意这一点(我可能是错误的)。

使用object提供的默认实现,除非您对一个类的引用相同,否则它们将彼此不相等。通过覆盖Equals和GetHashCode,您可以基于内部值而不是对象引用来报告相等性。


2
所述^ =方法不是用于生成哈希一个特别好的方法-它倾向于导致大量的公共/可预测的碰撞的-例如,如果PROP1 = PROP2 = 3
马克Gravell

如果值相同,则由于对象相等,因此碰撞不会出现问题。但是13 * Hash + NewHash似乎很有趣。
Bennett Dill

2
本:尝试它OBJ1 {PROP1 = 12,PROP2 = 12}和{OBJ2 PROP1 = 13,PROP2 = 13}
托马什卡夫卡

0

如果您只是在处理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;
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.