Usar el campo de un objeto como clave de diccionario genérica

120

Si quiero usar objetos como claves para a Dictionary, ¿qué métodos necesitaré anular para hacer que se comparen de una manera específica?

Digamos que tengo una clase que tiene propiedades:

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

    // elided
} 

Y quiero crear un:

Dictionary<Foo, List<Stuff>>

Quiero que los Fooobjetos con lo mismo FooIDse consideren del mismo grupo. ¿Qué métodos necesitaré anular en la Fooclase?

Para resumir: quiero categorizar Stuffobjetos en listas, agrupadas por Fooobjetos. Stufflos objetos tendrán una FooIDpara vincularlos a su categoría.

Dana
fuente

Respuestas:

151

De forma predeterminada, los dos métodos importantes son 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 lo quisieras como 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 un IEqualityComparer<T>para hacer lo mismo.

Marc Gravell
fuente
5
+1 y no pretendo secuestrar este hilo, pero tenía la impresión de que GetHashCode () debería devolver FooId.GetHashCode (). ¿No es este el patrón correcto?
Ken Browning
8
@Ken: bueno, solo necesita devolver un int que proporcione 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.
Marc Gravell
2
¡Gracias! Fui con IEqualityComparer <T> ya que era solo para el Dicarionary que necesitaba los métodos anulados.
Dana
1
Debe tener en cuenta que el rendimiento de los contenedores basados ​​en tablas hash (Dictionary <T>, Dictionary, HashTable, etc.) depende de la calidad de la función hash utilizada. Si simplemente utiliza FooID como código hash, los contenedores pueden funcionar muy mal.
Jørgen Fogh
2
@ JørgenFogh Soy muy consciente de eso; el ejemplo presentado es consistente con la intención declarada. Hay muchas preocupaciones relacionadas con la inmutabilidad del hash; Los identificadores cambian con menos frecuencia que los nombres y, por lo general , son indicadores de equivalencia únicos y fiables. Sin embargo, es un tema no trivial.
Marc Gravell
33

Como desea FooIDque sea el identificador del grupo, debe usarlo como clave en el diccionario en lugar del objeto Foo:

Dictionary<int, List<Stuff>>

Si usara el Fooobjeto como clave, simplemente implementaría el método GetHashCodey Equalspara considerar solo la FooIDpropiedad. La Namepropiedad sería un peso muerto en lo que Dictionaryrespecta a, por lo que solo se usaría Foocomo envoltorio para un int.

Por lo tanto, es mejor usar el FooIDvalor directamente, y luego no tiene que implementar nada, ya que Dictionaryya admite el uso de intcomo clave.

Editar:
si desea usar la Fooclase como clave de todos modos, 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());
Guffa
fuente
1
Más correctamente, int ya admite los métodos / interfaces necesarios para que se utilice como clave. Dictionary no tiene conocimiento directo de int ni de ningún otro tipo.
Jim Mischel
Pensé en eso, pero por una variedad de razones, era más limpio y más conveniente usar los objetos como claves del diccionario.
Dana
1
Bueno, solo parece que estás usando el objeto como clave, ya que en realidad solo estás usando la identificación como clave.
Guffa
8

Para Foo, deberá anular object.GetHashCode () y object.Equals ()

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

Asegúrese de calcular buenos códigos hash (evite que muchos objetos Foo iguales tengan el mismo código hash), pero asegúrese de que dos Foos iguales tengan el mismo código hash. Es posible que desee comenzar con Equals-Method y luego (en GetHashCode ()) xo ​​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();
     }
}
froh42
fuente
2
Aparte, pero xor (^) es un mal combinador de códigos hash, ya que a menudo conduce a muchas colisiones diagonales (es decir, {"foo", "bar"} vs {"bar", "foo"}. Un mejor la opción es multiplicar y sumar cada término, es decir, 17 * a.GetHashCode () + B.GetHashCode ();
Marc Gravell
2
Marc, veo lo que quieres decir. Pero, ¿cómo se llega al número mágico 17? ¿Es ventajoso usar un número primo como multiplicador para combinar hashes? Si es así, ¿por qué?
froh42
¿Puedo sugerir regresar: (A + B) .GetHashCode () en lugar de: 17 * A.GetHashCode () + B.GetHashCode () Esto: 1) Será menos probable que haya una colisión y 2) se asegurará de que no haya desbordamiento de enteros.
Charles Burns
(A + B) .GetHashCode () es un algoritmo hash muy malo, ya que diferentes conjuntos de (A, B) pueden dar como resultado el mismo hash si se concatenan en la misma cadena; "hellow" + "ned" es lo mismo que "hell" + "poseído" y daría como resultado el mismo hash.
Kaesve
@kaesve ¿qué tal (A + "" + B) .GetHashCode ()?
Timeless
1

¡Qué hay de la Hashtableclase!

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

De la forma anterior, puede usar cualquier objeto (su objeto de clase) como una clave de diccionario genérica :)

Behzad Ebrahimi
fuente
1

Yo tuve el mismo problema. Ahora puedo usar cualquier objeto que haya probado como clave debido a que anula Equals 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 objeto 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;
    }
}

Así es como se usa 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);
        }
    }
kb4000
fuente