Używanie pola obiektu jako ogólnego klucza Dictionary

120

Jeśli chcę używać obiektów jako kluczy dla a Dictionary, jakie metody będę musiał przesłonić, aby porównać je w określony sposób?

Powiedzmy, że mam klasę, która ma właściwości:

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

    // elided
} 

I chcę stworzyć:

Dictionary<Foo, List<Stuff>>

Chcę, aby Fooobiekty z tym samym FooIDbyły traktowane jako ta sama grupa. Jakie metody będę musiał przesłonić w Fooklasie?

Podsumowując: chcę podzielić Stuffobiekty na listy, pogrupowane według Fooobiektów. Stuffobiekty będą miały FooIDlink do ich kategorii.

Dana
źródło

Odpowiedzi:

151

Domyślnie dwie ważne metody to GetHashCode()i Equals(). Ważne jest, aby jeśli dwie rzeczy były równe ( Equals()zwraca prawdę), to miały ten sam kod skrótu. Na przykład możesz „zwrócić FooID;” jak GetHashCode()jeśli chcesz, aby to było dopasowanie. Możesz również wdrożyć IEquatable<Foo>, ale jest to opcjonalne:

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

Wreszcie inną alternatywą jest zapewnienie IEqualityComparer<T>możliwości zrobienia tego samego.

Marc Gravell
źródło
5
+1 i nie mam zamiaru przechwytywać tego wątku, ale miałem wrażenie, że GetHashCode () powinien zwrócić FooId.GetHashCode (). Czy to nie jest właściwy wzór?
Ken Browning
8
@Ken - cóż, wystarczy zwrócić int, który zapewnia niezbędne funkcje. Który FooID zrobi tak samo dobrze jak FooID.GetHashCode (). Jako szczegół implementacji, Int32.GetHashCode () to „return this;”. W przypadku innych typów (string itp.), To tak: .GetHashCode () byłoby bardzo przydatne.
Marc Gravell
2
Dzięki! Poszedłem z IEqualityComparer <T>, ponieważ tylko dla Dicarionary potrzebowałem nadpisanych metod.
Dana
1
Należy pamiętać, że wydajność kontenerów opartych na tabelach skrótów (Dictionary <T>, Dictionary, HashTable itp.) Zależy od jakości używanej funkcji skrótu. Jeśli po prostu FooID jako kod skrótu, kontenery mogą działać bardzo słabo.
Jørgen Fogh
2
@ JørgenFogh Jestem tego bardzo świadomy; przedstawiony przykład jest zgodny z podaną intencją. Istnieje wiele powiązanych problemów związanych z niezmiennością wartości skrótu; Identyfikatory zmieniają się rzadziej niż nazwy i są zwykle niezawodnie unikalne i są wskaźnikami równoważności. Jednak temat nietrywialny.
Marc Gravell
33

Ponieważ chcesz, FooIDaby był identyfikatorem grupy, powinieneś użyć tego jako klucza w słowniku zamiast obiektu Foo:

Dictionary<int, List<Stuff>>

Jeśli chcesz użyć Fooobiektu jako klucza, po prostu zaimplementowałbyś metodę GetHashCodeand Equals, aby uwzględnić tylko FooIDwłaściwość. NameNieruchomość będzie tylko martwy ciężar w miarę Dictionarychodzi, więc po prostu użyć Foojako wrapper dla int.

Dlatego lepiej jest użyć FooIDwartości bezpośrednio, a wtedy nie musisz niczego implementować, ponieważ Dictionaryjuż obsługuje używanie intklucza jako klucza.

Edycja:
jeśli Foomimo wszystko chcesz użyć klasy jako klucza, IEqualityComparer<Foo>jest to łatwe do zaimplementowania:

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

Stosowanie:

Dictionary<Foo, List<Stuff>> dict = new Dictionary<Foo, List<Stuff>>(new FooEqualityComparer());
Guffa
źródło
1
Bardziej poprawnie, int już obsługuje metody / interfejsy wymagane do użycia go jako klucza. Słownik nie ma bezpośredniej wiedzy na temat int ani żadnego innego typu.
Jim Mischel
Myślałem o tym, ale z różnych powodów używanie obiektów jako kluczy słownika było czystsze i wygodniejsze.
Dana
1
Cóż, wygląda na to, że używasz obiektu jako klucza, ponieważ tak naprawdę używasz tylko identyfikatora jako klucza.
Guffa
8

W przypadku Foo musisz nadpisać object.GetHashCode () i object.Equals ()

Słownik wywoła GetHashCode (), aby obliczyć przedział mieszania dla każdej wartości i Equals, aby porównać, czy dwa Foo są identyczne.

Upewnij się, że obliczasz dobre kody skrótu (unikaj wielu równych obiektów Foo mających ten sam kod skrótu), ale upewnij się, że dwa równe Foos mają ten sam kod skrótu. Możesz zacząć od metody Equals, a następnie (w GetHashCode ()) xorować kod skrótu każdego elementu członkowskiego, który porównujesz w 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
źródło
2
Poza tym - ale xor (^) jest słabym kombinatorem dla kodów mieszających, ponieważ często prowadzi do wielu kolizji po przekątnej (np. {"Foo", "bar"} vs {"bar", "foo"}. Lepiej wybór polega na pomnożeniu i dodaniu każdego terminu - tj. 17 * a.GetHashCode () + B.GetHashCode ();
Marc Gravell
2
Marc, rozumiem, co masz na myśli. Ale jak dostać się do magicznej liczby 17? Czy warto używać liczby pierwszej jako mnożnika do łączenia haszów? Jeśli tak, dlaczego?
froh42
Czy mogę zasugerować zwrócenie: (A + B) .GetHashCode () zamiast: 17 * A.GetHashCode () + B.GetHashCode () To: 1) zmniejszy prawdopodobieństwo kolizji i 2) zapewni, że nie ma całkowitą przepełnienie.
Charles Burns
(A + B) .GetHashCode () tworzy bardzo zły algorytm haszowania, ponieważ różne zestawy (A, B) mogą dać ten sam hash, jeśli są połączone z tym samym ciągiem; „hellow” + „ned” to to samo, co „hell” + „own” i skutkuje tym samym hashem.
kaesve
@kaesve a co powiesz na (A + "" + B) .GetHashCode ()?
Ponadczasowy
1

A co z Hashtableklasą!

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

W powyższy sposób możesz użyć dowolnego obiektu (obiektu Twojej klasy) jako ogólnego klucza Dictionary :)

Behzad Ebrahimi
źródło
1

Miałem ten sam problem. Teraz mogę użyć dowolnego obiektu, który wypróbowałem, jako klucza z powodu zastąpienia Equals i GetHashCode.

Oto klasa, którą zbudowałem z metodami do użycia wewnątrz przesłonięć Equals (obiekt obj) i GetHashCode (). Zdecydowałem się użyć typów ogólnych i algorytmu haszującego, który powinien być w stanie objąć większość obiektów. Daj mi znać, jeśli zobaczysz tutaj coś, co nie działa w przypadku niektórych typów obiektów i masz sposób, aby to poprawić.

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

Oto jak jest używany na zajęciach:

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
źródło