2009-03-11 14 views
101

Si quiero usar objetos como las claves para un Dictionary, ¿qué métodos debo anular para que se puedan comparar de una manera específica?Usar un objeto como una clave de diccionario genérica

decir que tengo una clase que tiene propiedades:

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

    // elided 
} 

Y quiero crear una:

Dictionary<Foo, List<Stuff>> 

Quiero Foo objetos con el mismo FooID a ser considerado el mismo grupo. ¿Qué métodos necesitaré anular en la clase Foo?

En resumen: quiero categorizar Stuff objetos en listas, agrupadas por objetos Foo. Stuff objetos tendrán un FooID para vincularlos a su categoría.

Respuesta

128

Por defecto, los dos métodos son importantes GetHashCode() y Equals(). Es importante que si dos cosas son iguales (Equals() devuelve verdadero), tengan el mismo código hash. Por ejemplo, puede "devolver FooID"; como GetHashCode() si quieres eso como el partido. También puede implementar IEquatable<Foo>, pero eso es opcional:

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

Por último, otra alternativa es proporcionar una IEqualityComparer<T> a hacer lo mismo.

+4

+1 y no me refiero a secuestrar este hilo, pero tenía la impresión de que GetHashCode() debería devolver FooId.GetHashCode(). ¿No es este el patrón correcto? –

+7

@Ken - bueno, solo necesita devolver un int que proporciona las características necesarias. Qué FooID funcionará tan bien como FooID.GetHashCode(). Como detalle de implementación, Int32.GetHashCode() es "return this;". Para otros tipos (cadena, etc.), entonces sí: .GetHashCode() sería muy útil. –

+2

Gracias! Fui con IEqualityComparer ya que era solo para Dicarionary que necesitaba los métodos modificados. – Dana

8

Para Foo tendrá que anular object.GetHashCode() y() Object.equals

El diccionario llamará GetHashCode() para calcular un cubo hash para cada valor y Equivale a comparar si dos de Foo son idénticos .

Asegúrese de calcular buenos códigos hash (evite muchos objetos iguales Foo que tengan el mismo código hash), pero asegúrese de que dos iguales Foos tengan el mismo código hash. Es posible que desee comenzar con Equals-Method y luego (en GetHashCode()) xor el código hash de cada miembro que compare en 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(); 
    } 
} 
+2

Aparte - pero XOR (^) hace un combinador pobre para hash códigos, ya que a menudo conduce a una gran cantidad de colisiones diagonales (es decir, {"foo", "bar"} vs {"bar", "foo"}. Una mejor opción es multiplicar y agregar cada término, es decir, 17 * a.GetHashCode() + B.GetHashCode(); –

+2

Marc, I mira a qué te refieres. Pero ¿cómo consigues el número mágico 17? ¿Es ventajoso usar un número primo como un multiplicador para combinar hash? Si es así, ¿por qué? – froh42

+0

Puedo sugerir que vuelvas: (A + B) .GetHashCode() en lugar de: 17 * A.GetHashCode() + B.GetHashCode() Esto: 1) tendrá menos probabilidades de tener una colisión y 2) se asegurará de que no haya desbordamiento de enteros. –

29

Como desea que el FooID a ser el identificador para el grupo, usted debe usar eso como clave en el diccionario en lugar del objeto Foo:

Dictionary<int, List<Stuff>> 

Si desea utilizar el objeto Foo como clave, simplemente implementaría el método GetHashCode y Equals para considerar solo la propiedad FooID. La propiedad Name sería solo peso muerto en lo que respecta al Dictionary, por lo que solo usaría Foo como contenedor para un int.

Por lo tanto, es mejor utilizar el valor FooID directamente, y luego no tiene que implementar nada ya que Dictionary ya admite el uso de int como clave.

Editar:
Si desea utilizar la clase Foo como la clave de todos modos, la IEqualityComparer<Foo> es fácil de implementar:

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

Uso:

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

Más correctamente, int ya admite los métodos/interfaces necesarios para que se use como clave. El diccionario no tiene conocimiento directo de int o de ningún otro tipo. –

+0

Pensé en eso, pero por una variedad de razones, era más limpio y más conveniente usar los objetos como las teclas del diccionario. – Dana

+1

Bueno, solo parece que estás usando el objeto como clave, ya que solo estás usando el ID como clave. – Guffa

1

¿Qué hay de Hashtable clase!

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

En forma anterior, se puede utilizar cualquier objeto (el objeto de clase) como una clave genérica Diccionario :)

0

que tenía el mismo problema. Ahora puedo usar cualquier objeto que haya intentado como clave debido a la anulación de Iguales y GetHashCode.

Aquí hay una clase que construí con métodos para usar dentro de las anulaciones de Equals (object obj) y GetHashCode(). Decidí usar genéricos y un algoritmo hash que debería poder cubrir la mayoría de los objetos. Por favor, avíseme si ve algo aquí que no funciona para algunos tipos de objetos y tiene una forma de mejorarlo.

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

Aquí es cómo se utiliza en una clase:

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

    public override int GetHashCode() 
    { 
     unchecked 
     { 
      return new Equality<ClassName>().GetHashCode(this); 
     } 
    } 
Cuestiones relacionadas