使用对象的字段作为通用词典密钥


120

如果我想使用对象作为a的键Dictionary,我需要重写哪些方法以使其以特定方式进行比较?

说我有一个具有属性的类:

class Foo {
    public string Name { get; set; }
    public int FooID { get; set; }

    // elided
} 

我想创建一个:

Dictionary<Foo, List<Stuff>>

我希望Foo具有相同对象的对象FooID被视为同一组。我需要在Foo该类中重写哪些方法?

总结一下:我想按Stuff对象将Foo对象分类为列表。Stuff对象将具有一个FooID将它们链接到其类别的链接。

Answers:


151

默认情况下,两个重要方法是GetHashCode()Equals()。重要的是,如果两个条件相等(Equals()返回true),则它们具有相同的哈希码。例如,您可以“返回FooID;”。因为GetHashCode(),如果你想,随着比赛。您也可以实现IEquatable<Foo>,但这是可选的:

class Foo : IEquatable<Foo> {
    public string Name { get; set;}
    public int FooID {get; set;}

    public override int GetHashCode() {
        return FooID;
    }
    public override bool Equals(object obj) {
        return Equals(obj as Foo);
    }
    public bool Equals(Foo obj) {
        return obj != null && obj.FooID == this.FooID;
    }
}

最后,另一种选择是提供IEqualityComparer<T>相同的功能。


5
+1并且我并不是要劫持该线程,但我的印象是GetHashCode()应该返回FooId.GetHashCode()。这不是正确的模式吗?
肯·布朗宁

8
@Ken-好吧,只需要返回一个提供必要功能的int即可。哪个FooID和FooID.GetHashCode()一样好。作为实现细节,Int32.GetHashCode()是“返回此;”。对于其他类型(字符串等),则可以:.GetHashCode()将非常有用。
Marc Gravell

2
谢谢!我使用IEqualityComparer <T>,因为仅对于Dicarionary,我需要重写的方法。
达娜(Dana)

1
您应该意识到,基于哈希表(Dictionary <T>,Dictionary,HashTable等)的容器的性能取决于所使用的哈希函数的质量。如果仅将FooID作为哈希码,则容器的性能可能会很差。
约尔根·福格2014年

2
@JørgenFogh我很清楚这一点;所提供的示例与所述意图一致。有很多与哈希不变性相关的问题;id的更改少于名称的更改,并且通常可靠地是唯一的和等效的指示符。不过,这是一个不平凡的主题。
马克·格雷夫

33

由于您希望FooID成为组的标识符,因此应将其用作字典中的键而不是Foo对象:

Dictionary<int, List<Stuff>>

如果将Foo对象用作键,则只需实现GetHashCodeand Equals方法即可仅考虑该FooID属性。该Name属性将仅是Dictionary所涉及的自重,因此您仅可以Foo用作的包装int

因此,最好FooID直接使用该值,然后不必执行任何操作,因为Dictionary已经支持使用an int作为键。

编辑:
如果您Foo仍然想使用该类作为键,IEqualityComparer<Foo>则易于实现:

public class FooEqualityComparer : IEqualityComparer<Foo> {
   public int GetHashCode(Foo foo) { return foo.FooID.GetHashCode(); }
   public bool Equals(Foo foo1, Foo foo2) { return foo1.FooID == foo2.FooID; }
}

用法:

Dictionary<Foo, List<Stuff>> dict = new Dictionary<Foo, List<Stuff>>(new FooEqualityComparer());

1
更正确地说,int已经支持将其用作键所需的方法/接口。字典不具有int或任何其他类型的直接知识。
Jim Mischel

我曾考虑过这一点,但是由于多种原因,使用对象作为字典键更加干净方便。
达娜(Dana)

1
好吧,看起来您好像只是在使用对象作为键,因为您实际上只是在使用id作为键。
Guffa

8

对于Foo,您将需要覆盖object.GetHashCode()和object.Equals()

字典将调用GetHashCode()来为每个值计算一个哈希桶,并等于来比较两个Foo是否相同。

确保计算出良好的哈希码(避免使用相同的哈希码的多个相等的Foo对象),但请确保两个等于Foos的哈希码相同。您可能想从Equals-Method开始,然后(在GetHashCode()中)对您在Equals中比较的每个成员的哈希码进行xor处理。

public class Foo { 
     public string A;
     public string B;

     override bool Equals(object other) {
          var otherFoo = other as Foo;
          if (otherFoo == null)
             return false;
          return A==otherFoo.A && B ==otherFoo.B;
     }

     override int GetHashCode() {
          return 17 * A.GetHashCode() + B.GetHashCode();
     }
}

2
撇开-但是xor(^)对于哈希码来说是一个较差的组合器,因为它经常导致许多对角线冲突(即{“ foo”,“ bar”}与{“ bar”,“ foo”}。选择是将每个项相乘并相加-即17 * a.GetHashCode()+ B.GetHashCode();
Marc Gravell

2
马克,我明白你的意思。但是,您如何获得魔法数字17?将质数用作乘法器以组合哈希是否有利?如果是这样,为什么?
froh42

我可以建议返回:(A + B).GetHashCode()而不是:17 * A.GetHashCode()+ B.GetHashCode()这将:1)减少碰撞的可能性,并2)确保没有碰撞整数溢出。
查尔斯·伯恩斯

(A + B).GetHashCode()使哈希算法非常糟糕,因为如果(A,B)的不同集合连接到相同的字符串,则它们可能导致相同的哈希;“ hellow” +“ ned”与“ hell” +“ owned”相同,并且会导致相同的哈希。
kaesve 2014年

@kaesve(A +“” + B).GetHashCode()怎么样?
永恒的2014年

1

什么Hashtable类的!

Hashtable oMyDic = new Hashtable();
Object oAnyKeyObject = null;
Object oAnyValueObject = null;
oMyDic.Add(oAnyKeyObject, oAnyValueObject);
foreach (DictionaryEntry de in oMyDic)
{
   // Do your job
}

通过以上方法,您可以将任何对象(您的类对象)用作通用的Dictionary键:)


1

我有同样的问题。由于覆盖了Equals和GetHashCode,我现在可以将尝试使用的任何对象用作键。

这是我使用方法构建的类,该方法可在Equals(object obj)和GetHashCode()的重写内部使用。我决定使用泛型和哈希算法,该算法应该能够覆盖大多数对象。如果您在这里看到不适用于某些类型的对象的任何东西,并且有改进的方法,请告诉我。

public class Equality<T>
{
    public int GetHashCode(T classInstance)
    {
        List<FieldInfo> fields = GetFields();

        unchecked
        {
            int hash = 17;

            foreach (FieldInfo field in fields)
            {
                hash = hash * 397 + field.GetValue(classInstance).GetHashCode();
            }
            return hash;
        }
    }

    public bool Equals(T classInstance, object obj)
    {
        if (ReferenceEquals(null, obj))
        {
            return false;
        }
        if (ReferenceEquals(this, obj))
        {
            return true;
        }
        if (classInstance.GetType() != obj.GetType())
        {
            return false;
        }

        return Equals(classInstance, (T)obj);
    }

    private bool Equals(T classInstance, T otherInstance)
    {
        List<FieldInfo> fields = GetFields();

        foreach (var field in fields)
        {
            if (!field.GetValue(classInstance).Equals(field.GetValue(otherInstance)))
            {
                return false;
            }
        }

        return true;
    }

    private List<FieldInfo> GetFields()
    {
        Type myType = typeof(T);

        List<FieldInfo> fields = myType.GetTypeInfo().DeclaredFields.ToList();
        return fields;
    }
}

这是在类中的用法:

public override bool Equals(object obj)
    {
        return new Equality<ClassName>().Equals(this, obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return new Equality<ClassName>().GetHashCode(this);
        }
    }
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.