使用对象的字段作为通用字典键

Posted

技术标签:

【中文标题】使用对象的字段作为通用字典键【英文标题】:Using the field of an object as a generic Dictionary key 【发布时间】:2010-10-12 16:47:00 【问题描述】:

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

假设我有一个具有属性的类:

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

    // elided
 

我想创建一个:

Dictionary<Foo, List<Stuff>>

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

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

【问题讨论】:

【参考方案1】:

默认情况下,两个重要的方法是GetHashCode()Equals()。重要的是,如果两个事物相等(Equals() 返回 true),它们具有相同的哈希码。例如,您可能会“返回 FooID;”作为GetHashCode(),如果您希望它作为匹配项。你也可以实现IEquatable&lt;Foo&gt;,但这是可选的:

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&lt;T&gt; 来做同样的事情。

【讨论】:

+1 我并不是要劫持这个线程,但我的印象是 GetHashCode() 应该返回 FooId.GetHashCode()。这不是正确的模式吗? @Ken - 好吧,它只需要返回一个提供必要功能的 int 。哪个 FooID 与 FooID.GetHashCode() 一样好。作为一个实现细节,Int32.GetHashCode() 是“return this;”。对于其他类型(字符串等),是的:.GetHashCode() 将非常有用。 谢谢!我选择了 IEqualityComparer,因为只有 Dicarionary 才需要重写方法。 您应该知道,基于哈希表(Dictionary、Dictionary、HashTable 等)的容器的性能取决于所使用的哈希函数的质量。如果您只是将 FooID 作为哈希码,容器的性能可能会很差。 @JørgenFogh 我非常清楚这一点;提供的示例与所述意图一致。有很多与哈希不变性相关的问题; id 的更改频率低于名称,并且通常是可靠的唯一性和等效性指标。不过,这是一个不平凡的主题。【参考方案2】:

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

Dictionary<int, List<Stuff>>

如果您使用Foo 对象作为键,您只需实现GetHashCodeEquals 方法以仅考虑FooID 属性。就Dictionary 而言,Name 属性只是自重,因此您只需使用Foo 作为int 的包装器。

因此最好直接使用FooID 值,然后您不必实现任何东西,因为Dictionary 已经支持使用int 作为键。

编辑: 如果你想使用Foo类作为key,IEqualityComparer&lt;Foo&gt;很容易实现:

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());

【讨论】:

更准确地说,int 已经支持将其用作键所需的方法/接口。字典不直接了解 int 或任何其他类型。 我想过这个,但由于各种原因,使用对象作为字典键更清洁、更方便。 好吧,看起来您只是使用对象作为键,因为您实际上只是使用 id 作为键。【参考方案3】:

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

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

确保计算好的哈希码(避免许多相等的 Foo 对象具有相同的哈希码),但要确保两个相等的 Foo 具有相同的哈希码。您可能希望从 Equals 方法开始,然后(在 GetHashCode() 中)异或在 Equals 中比较的每个成员的哈希码。

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();
     

【讨论】:

除此之外 - 但是 xor (^) 对哈希码的组合很差,因为它经常导致很多对角线冲突(即 "foo","bar" vs "bar" ,"foo"。更好的选择是将每个项相乘和相加 - 即 17 * a.GetHashCode() + B.GetHashCode(); 马克,我明白你的意思了。但是你是怎么得到这个神奇的数字 17 的呢?使用素数作为组合哈希的乘数是否有利?如果是,为什么? 我可以建议返回:(A + B).GetHashCode() 而不是:17 * A.GetHashCode() + B.GetHashCode() 这将:1)不太可能发生冲突和 2) 确保没有整数溢出。 (A + B).GetHashCode() 是一个非常糟糕的散列算法,因为如果 (A, B) 的不同集合连接到相同的字符串,它们会产生相同的散列; "hellow" + "ned" 与 "hell" + "owned" 相同,并且会产生相同的哈希值。 @kaesve (A+" "+B).GetHashCode() 怎么样?【参考方案4】:

Hashtable 类呢!

Hashtable oMyDic = new Hashtable();
Object oAnyKeyObject = null;
Object oAnyValueObject = null;
oMyDic.Add(oAnyKeyObject, oAnyValueObject);
foreach (DictionaryEntry de in oMyDic)

   // Do your job

通过上述方式,您可以使用任何对象(您的类对象)作为通用字典键:)

【讨论】:

【参考方案5】:

我遇到了同样的问题。由于覆盖了 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);
        
    

【讨论】:

以上是关于使用对象的字段作为通用字典键的主要内容,如果未能解决你的问题,请参考以下文章

通用字典类键值

在 C# 中:如何声明一个类型为键的通用字典,该类型的 IEnumerable<> 作为值?

获取通用字典的指定值的多个键?

Swift - 具有实现通用协议的值的 Typealias 字典

在 Flow 中声明一个通用形状的对象

Spring Boot 通用对象列表比较和去重