Verwenden des Felds eines Objekts als generischer Wörterbuchschlüssel

120

Wenn ich Objekte als Schlüssel für a verwenden möchte Dictionary, welche Methoden muss ich überschreiben, damit sie auf bestimmte Weise verglichen werden?

Angenommen, ich habe eine Klasse mit Eigenschaften:

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

    // elided
} 

Und ich möchte ein erstellen:

Dictionary<Foo, List<Stuff>>

Ich möchte, dass FooObjekte mit FooIDderselben Gruppe als dieselbe Gruppe betrachtet werden. Welche Methoden muss ich in der FooKlasse überschreiben ?

Zusammenfassend: Ich möchte StuffObjekte in Listen kategorisieren , die nach FooObjekten gruppiert sind. StuffObjekte müssen FooIDsie mit ihrer Kategorie verknüpfen.

Dana
quelle

Antworten:

151

Standardmäßig sind die beiden wichtigen Methoden GetHashCode()und Equals(). Es ist wichtig, dass zwei Dinge, die gleich sind ( Equals()true zurückgeben), denselben Hash-Code haben. Beispielsweise könnten Sie "FooID zurückgeben"; als GetHashCode()wenn du das als Match willst. Sie können auch implementieren IEquatable<Foo>, dies ist jedoch optional:

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

Schließlich besteht eine andere Alternative darin, IEqualityComparer<T>das Gleiche zu tun.

Marc Gravell
quelle
5
+1 und ich wollte diesen Thread nicht entführen, aber ich hatte den Eindruck, dass GetHashCode () FooId.GetHashCode () zurückgeben sollte. Ist das nicht das richtige Muster?
Ken Browning
8
@ Ken - nun, es muss nur ein int zurückgegeben werden, das die notwendigen Funktionen bietet. Welche FooID funktioniert genauso gut wie FooID.GetHashCode (). Als Implementierungsdetail lautet Int32.GetHashCode () "return this;". Für andere Typen (Zeichenfolge usw.) wäre ja: .GetHashCode () sehr nützlich.
Marc Gravell
2
Vielen Dank! Ich habe mich für den IEqualityComparer <T> entschieden, da ich nur für den Dicarionary die Overriden-Methoden benötigte.
Dana
1
Sie sollten sich bewusst sein, dass die Leistung von Containern, die auf Hashtabellen basieren (Dictionary <T>, Dictionary, HashTable usw.), von der Qualität der verwendeten Hash-Funktion abhängt. Wenn Sie einfach FooID als Hash-Code verwenden, können die Container eine sehr schlechte Leistung erbringen.
Jørgen Fogh
2
@ JørgenFogh Das ist mir sehr bewusst; Das vorgestellte Beispiel stimmt mit der angegebenen Absicht überein. Es gibt viele verwandte Bedenken in Bezug auf die Unveränderlichkeit von Hashs. IDs ändern sich seltener als Namen und sind in der Regel zuverlässig eindeutig und Indikatoren für die Äquivalenz. Ein nicht triviales Thema.
Marc Gravell
33

Da FooIDdies der Bezeichner für die Gruppe sein soll, sollten Sie diesen als Schlüssel im Wörterbuch anstelle des Foo-Objekts verwenden:

Dictionary<int, List<Stuff>>

Wenn Sie das FooObjekt als Schlüssel verwenden würden, würden Sie nur die Methode GetHashCodeund implementieren Equals, um nur die FooIDEigenschaft zu berücksichtigen . Die NameEigenschaft wäre nur totes Gewicht, was das Dictionarybetrifft, so würden Sie nur Fooals Wrapper für eine verwenden int.

Daher ist es besser, den FooIDWert direkt zu verwenden, und dann müssen Sie nichts implementieren, da dies Dictionarybereits die Verwendung von a intals Schlüssel unterstützt.

Bearbeiten:
Wenn Sie die FooKlasse trotzdem als Schlüssel verwenden möchten , IEqualityComparer<Foo>ist das einfach zu implementieren:

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

Verwendung:

Dictionary<Foo, List<Stuff>> dict = new Dictionary<Foo, List<Stuff>>(new FooEqualityComparer());
Guffa
quelle
1
Richtigerweise unterstützt int bereits die Methoden / Schnittstellen, die für die Verwendung als Schlüssel erforderlich sind. Das Wörterbuch hat keine direkten Kenntnisse über int oder einen anderen Typ.
Jim Mischel
Ich habe darüber nachgedacht, aber aus verschiedenen Gründen war es sauberer und bequemer, die Objekte als Wörterbuchschlüssel zu verwenden.
Dana
1
Nun, es sieht nur so aus, als würden Sie das Objekt als Schlüssel verwenden, da Sie wirklich nur die ID als Schlüssel verwenden.
Guffa
8

Für Foo müssen Sie object.GetHashCode () und object.Equals () überschreiben.

Das Wörterbuch ruft GetHashCode () auf, um einen Hash-Bucket für jeden Wert zu berechnen, und Equals, um zu vergleichen, ob zwei Foo identisch sind.

Stellen Sie sicher, dass Sie gute Hash-Codes berechnen (vermeiden Sie viele gleiche Foo-Objekte mit demselben Hash-Code), aber stellen Sie sicher, dass zwei gleiche Foos denselben Hash-Code haben. Vielleicht möchten Sie mit der Equals-Methode beginnen und dann (in GetHashCode ()) xor den Hash-Code jedes Mitglieds, das Sie in Equals vergleichen.

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
quelle
2
Nebenbei - aber xor (^) ist ein schlechter Kombinator für Hash-Codes, da es oft zu vielen diagonalen Kollisionen führt (dh {"foo", "bar"} vs {"bar", "foo"}. Ein besserer Sie können jeden Begriff multiplizieren und addieren - dh 17 * a.GetHashCode () + B.GetHashCode ();
Marc Gravell
2
Marc, ich verstehe was du meinst. Aber wie kommt man zur magischen Nummer 17? Ist es vorteilhaft, eine Primzahl als Multiplikator zum Kombinieren von Hashes zu verwenden? Wenn ja warum?
froh42
Darf ich vorschlagen, Folgendes zurückzugeben: (A + B) .GetHashCode () statt: 17 * A.GetHashCode () + B.GetHashCode () Dies wird: 1) weniger wahrscheinlich eine Kollision haben und 2) sicherstellen, dass es keine gibt Ganzzahlüberlauf.
Charles Burns
(A + B) .GetHashCode () führt zu einem sehr schlechten Hashing-Algorithmus, da verschiedene Sätze von (A, B) zu demselben Hash führen können, wenn sie mit derselben Zeichenfolge verknüpft sind. "hellow" + "ned" ist dasselbe wie "hell" + "own" und würde zu demselben Hash führen.
Kaesve
@kaesve wie wäre es mit (A + "" + B) .GetHashCode ()?
Zeitlos
1

Was ist mit HashtableKlasse!

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

Auf die oben beschriebene Weise können Sie jedes Objekt (Ihr Klassenobjekt) als generischen Wörterbuchschlüssel verwenden :)

Behzad Ebrahimi
quelle
1

Ich hatte das gleiche Problem. Ich kann jetzt jedes Objekt, das ich versucht habe, als Schlüssel verwenden, da Equals und GetHashCode überschrieben werden.

Hier ist eine Klasse, die ich mit Methoden erstellt habe, die innerhalb der Überschreibungen von Equals (object obj) und GetHashCode () verwendet werden können. Ich entschied mich für Generika und einen Hashing-Algorithmus, der die meisten Objekte abdecken sollte. Bitte lassen Sie mich wissen, wenn Sie hier etwas sehen, das für einige Objekttypen nicht funktioniert, und Sie haben eine Möglichkeit, es zu verbessern.

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

So wird es in einer Klasse verwendet:

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
quelle