Utilisation du champ d'un objet comme clé de dictionnaire générique

120

Si je veux utiliser des objets comme clés pour un Dictionary , quelles méthodes devrai-je remplacer pour les comparer d'une manière spécifique?

Disons que j'ai une classe qui a des propriétés:

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

    // elided
} 

Et je veux créer un:

Dictionary<Foo, List<Stuff>>

Je veux que les Fooobjets avec le même FooIDsoient considérés comme le même groupe. Quelles méthodes dois-je remplacer dans la Fooclasse?

Pour résumer: je souhaite classer les Stuffobjets en listes, groupées par Fooobjets. Stuffles objets auront un FooIDpour les lier à leur catégorie.

Dana
la source

Réponses:

151

Par défaut, les deux méthodes importantes sont GetHashCode()et Equals(). Il est important que si deux choses sont égales ( Equals()renvoie vrai), qu'elles aient le même code de hachage. Par exemple, vous pouvez "retourner FooID;" comme GetHashCode()si vous voulez que ce soit le match. Vous pouvez également implémenter IEquatable<Foo>, mais cela est facultatif:

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

Enfin, une autre alternative est de fournir un IEqualityComparer<T>pour faire de même.

Marc Gravell
la source
5
+1 et je ne veux pas détourner ce fil, mais j'avais l'impression que GetHashCode () devrait retourner FooId.GetHashCode (). N'est-ce pas le bon modèle?
Ken Browning
8
@Ken - eh bien, il suffit de renvoyer un int qui fournit les fonctionnalités nécessaires. Quel FooID fera aussi bien que FooID.GetHashCode (). En tant que détail d'implémentation, Int32.GetHashCode () est "return this;". Pour les autres types (chaîne, etc.), alors oui: .GetHashCode () serait très utile.
Marc Gravell
2
Merci! Je suis allé avec IEqualityComparer <T> car ce n'était que pour le Dicarionary que j'avais besoin des méthodes surchargées.
Dana
1
Sachez que les performances des conteneurs basés sur les hashtables (Dictionary <T>, Dictionary, HashTable, etc.) dépendent de la qualité de la fonction de hachage utilisée. Si vous utilisez simplement FooID comme code de hachage, les conteneurs peuvent fonctionner très mal.
Jørgen Fogh
2
@ JørgenFogh J'en suis très conscient; l'exemple présenté est conforme à l'intention déclarée. Il existe de nombreuses préoccupations liées à l'immuabilité du hachage; Les identifiants changent moins souvent que les noms et sont généralement des indicateurs uniques et fiables d'équivalence. Un sujet non trivial, cependant.
Marc Gravell
33

Comme vous voulez que le FooIDsoit l'identificateur du groupe, vous devez l'utiliser comme clé dans le dictionnaire au lieu de l'objet Foo:

Dictionary<int, List<Stuff>>

Si vous utilisiez l' Fooobjet comme clé, vous implémenteriez simplement la méthode GetHashCodeand Equalspour ne considérer que la FooIDpropriété. La Namepropriété serait juste un poids mort en ce qui Dictionaryconcerne le, donc vous ne l'utiliseriez Fooque comme wrapper pour un fichier int.

Par conséquent, il est préférable d'utiliser la FooIDvaleur directement, et vous n'avez alors rien à implémenter car le Dictionaryprend déjà en charge l'utilisation de an intcomme clé.

Edit:
Si vous voulez Fooquand même utiliser la classe comme clé, le IEqualityComparer<Foo>est facile à implémenter:

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

Usage:

Dictionary<Foo, List<Stuff>> dict = new Dictionary<Foo, List<Stuff>>(new FooEqualityComparer());
Guffa
la source
1
Plus correctement, int prend déjà en charge les méthodes / interfaces nécessaires pour qu'il soit utilisé comme clé. Le dictionnaire n'a aucune connaissance directe de int ou de tout autre type.
Jim Mischel
J'y ai pensé, mais pour diverses raisons, il était plus propre et plus pratique d'utiliser les objets comme clés du dictionnaire.
Dana
1
Eh bien, il semble que vous n'utilisiez l'objet que comme clé, car vous n'utilisez en réalité que l'identifiant comme clé.
Guffa
8

Pour Foo, vous devrez remplacer object.GetHashCode () et object.Equals ()

Le dictionnaire appellera GetHashCode () pour calculer un compartiment de hachage pour chaque valeur et Equals pour comparer si deux Foo sont identiques.

Assurez-vous de calculer de bons codes de hachage (évitez que de nombreux objets Foo égaux aient le même hashcode), mais assurez-vous que deux Foos égaux ont le même code de hachage. Vous voudrez peut-être commencer par la méthode Equals, puis (dans GetHashCode ()) xou le code de hachage de chaque membre que vous comparez dans 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
la source
2
Mis à part - mais xor (^) fait un mauvais combinateur pour les codes de hachage, car il conduit souvent à beaucoup de collisions diagonales (par exemple {"foo", "bar"} vs {"bar", "foo"}. le choix est de multiplier et d'ajouter chaque terme - soit 17 * a.GetHashCode () + B.GetHashCode ();
Marc Gravell
2
Marc, je vois ce que tu veux dire. Mais comment obtenez-vous le nombre magique 17? Est-il avantageux d'utiliser un nombre premier comme multiplicateur pour combiner des hachages? Si oui, pourquoi?
froh42
Puis-je suggérer de retourner: (A + B) .GetHashCode () plutôt que: 17 * A.GetHashCode () + B.GetHashCode () Cela: 1) sera moins susceptible d'avoir une collision et 2) garantira qu'il n'y a pas débordement d'entier.
Charles Burns
(A + B) .GetHashCode () fait un très mauvais algorithme de hachage, car différents ensembles de (A, B) peuvent entraîner le même hachage s'ils sont concaténés à la même chaîne; "hellow" + "ned" est identique à "hell" + "possédé" et donnerait le même hachage.
kaesve
@kaesve et (A + "" + B) .GetHashCode ()?
Intemporel
1

Et la Hashtableclasse!

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

De la manière ci-dessus, vous pouvez utiliser n'importe quel objet (votre objet de classe) comme clé de dictionnaire générique :)

Behzad Ebrahimi
la source
1

J'ai eu le même problème. Je peux maintenant utiliser n'importe quel objet que j'ai essayé comme clé en raison de la substitution de Equals et GetHashCode.

Voici une classe que j'ai construite avec des méthodes à utiliser à l'intérieur des substitutions de Equals (object obj) et GetHashCode (). J'ai décidé d'utiliser des génériques et un algorithme de hachage qui devrait pouvoir couvrir la plupart des objets. Veuillez me faire savoir si vous voyez quelque chose ici qui ne fonctionne pas pour certains types d'objets et que vous avez un moyen de l'améliorer.

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

Voici comment il est utilisé dans une classe:

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
la source