Object.GetHashCode()的默认实现

时间:2009-04-06 03:25:15

标签: .net hash gethashcode

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

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

6 个答案:

答案 0 :(得分:83)

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 的完整实施相当大,因此更容易链接到the C++ source code

答案 1 :(得分:81)

对于一个类,默认值基本上是引用相等,这通常很好。如果编写一个结构,更常见的是覆盖相等性(尤其是避免装箱),但是你总是编写一个结构很少见!

当覆盖相等时,您应始终匹配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(^)等,这可能很常见。

答案 2 :(得分:7)

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

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

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

答案 3 :(得分:3)

由于我找不到解释 为什么 的答案,因此对于自定义结构,我们应该覆盖GetHashCodeEquals,而 < em>为什么 ,默认实现“不太可能适合用作哈希表中的键”,因此我将保留指向this blog 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; }
}
  

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

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

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

答案 4 :(得分:1)

一般来说,如果您要覆盖Equals,则需要覆盖GetHashCode。原因是因为它们都用于比较类/结构的相等性。

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

if(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,您可以根据内部值而不是对象引用来报告相等性。

答案 5 :(得分:0)

如果您只是在处理POCO,则可以使用此实用程序来简化您的生活:

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;
    }
}

...

$("table tbody").on("change", "input[type=checkbox]", function (e) { 
    var currentCB = $(this);
    var isChecked = this.checked;
    
    if (currentCB.is(".groupHeadCheck")) {
        var allCbs = currentCB.closest('tr').find('[type="checkbox"]').prop('checked', isChecked);
    } else {
        var allCbs = currentCB.closest('tr').find('[type="checkbox"]');
        var allSlaves = allCbs.not(".groupHeadCheck");
        var master = allCbs.filter(".groupHeadCheck");
        var allChecked;
        if (!isChecked) {
            allChecked = false;
        } else {
            allChecked = allSlaves.filter(":checked").length === allSlaves.length;
        }
        master.prop("checked", allChecked);
    }
});

$(".groupHead").next().find("[type=checkbox]").change();