Utilizzo del campo di un oggetto come chiave Dictionary generica


120

Se voglio utilizzare gli oggetti come chiavi per a Dictionary, quali metodi dovrò sostituire per farli confrontare in un modo specifico?

Supponiamo di avere una classe che ha proprietà:

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

    // elided
} 

E voglio creare un:

Dictionary<Foo, List<Stuff>>

Voglio che gli Foooggetti con lo stesso FooIDsiano considerati lo stesso gruppo. Quali metodi dovrò sostituire nella Fooclasse?

Per riassumere: voglio classificare gli Stuffoggetti in elenchi, raggruppati per Foooggetti. Stuffgli oggetti avranno un FooIDcollegamento per collegarli alla loro categoria.

Risposte:


151

Per impostazione predefinita, i due metodi importanti sono GetHashCode()e Equals(). È importante che se due cose sono uguali ( Equals()restituisce true), hanno lo stesso codice hash. Ad esempio, potresti "restituire FooID;" come GetHashCode()se lo desideri come corrispondenza. Puoi anche implementare IEquatable<Foo>, ma è facoltativo:

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

Infine, un'altra alternativa è fornire IEqualityComparer<T>e fare lo stesso.


5
+1 e non intendo dirottare questo thread, ma avevo l'impressione che GetHashCode () dovesse restituire FooId.GetHashCode (). Non è questo il modello giusto?
Ken Browning,

8
@Ken - beh, deve solo restituire un int che fornisca le funzionalità necessarie. Quale FooID funzionerà altrettanto bene di FooID.GetHashCode (). Come dettaglio di implementazione, Int32.GetHashCode () è "return this;". Per altri tipi (stringa, ecc.), Allora sì: .GetHashCode () sarebbe molto utile.
Marc Gravell

2
Grazie! Sono andato con IEqualityComparer <T> poiché era solo per il Dicarionario che avevo bisogno dei metodi sostituiti.
Dana

1
È necessario essere consapevoli che le prestazioni dei contenitori basati su tabelle hash (Dictionary <T>, Dictionary, HashTable, ecc.) Dipendono dalla qualità della funzione hash utilizzata. Se si utilizza semplicemente FooID come codice hash, i contenitori potrebbero funzionare molto male.
Jørgen Fogh

2
@ JørgenFogh Ne sono molto consapevole; l'esempio presentato è coerente con l'intento dichiarato. Ci sono molte preoccupazioni correlate relative all'immutabilità dell'hash; gli ID cambiano meno spesso dei nomi e di solito sono univoci e indicatori di equivalenza affidabili. Un argomento non banale, però.
Marc Gravell

33

Poiché vuoi FooIDche sia l'identificatore per il gruppo, dovresti usarlo come chiave nel dizionario invece dell'oggetto Foo:

Dictionary<int, List<Stuff>>

Se utilizzassi l' Foooggetto come chiave, implementeresti semplicemente il metodo GetHashCodee Equalsper considerare solo la FooIDproprietà. La Nameproprietà sarebbe solo un peso morto per quanto Dictionaryriguardava, quindi dovresti usarla Foocome involucro per un file int.

Quindi è meglio usare il FooIDvalore direttamente e quindi non devi implementare nulla poiché Dictionarygià supporta l'utilizzo di un intcome chiave.

Modifica:
se vuoi comunque usare la Fooclasse come chiave, IEqualityComparer<Foo>è facile da implementare:

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
Più correttamente, int supporta già i metodi / interfacce necessari per essere utilizzato come chiave. Il dizionario non ha una conoscenza diretta di int o di qualsiasi altro tipo.
Jim Mischel

Ci ho pensato, ma per una serie di ragioni era più pulito e più comodo usare gli oggetti come chiavi del dizionario.
Dana

1
Bene, sembra che tu stia usando l'oggetto come chiave, poiché in realtà stai usando solo l'id come chiave.
Guffa

8

Per Foo dovrai sovrascrivere object.GetHashCode () e object.Equals ()

Il dizionario chiamerà GetHashCode () per calcolare un hash bucket per ogni valore e Equals per confrontare se due Foo sono identici.

Assicurati di calcolare buoni codici hash (evita molti oggetti Foo uguali con lo stesso codice hash), ma assicurati che due Foo uguali abbiano lo stesso codice hash. Potresti voler iniziare con il metodo Equals e quindi (in GetHashCode ()) xo ​​il codice hash di ogni membro che confronti in 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
A parte, ma xor (^) è un cattivo combinatore per codici hash, poiché spesso porta a molte collisioni diagonali (ad esempio {"foo", "bar"} vs {"bar", "foo"}. la scelta è moltiplicare e sommare ogni termine, ad esempio 17 * a.GetHashCode () + B.GetHashCode ();
Marc Gravell

2
Marc, capisco cosa intendi. Ma come si arriva al numero magico 17? È vantaggioso utilizzare un numero primo come moltiplicatore per combinare gli hash? Se è così, perché?
froh42

Posso suggerire di restituire: (A + B) .GetHashCode () invece di: 17 * A.GetHashCode () + B.GetHashCode () Ciò: 1) Avrà meno probabilità di avere una collisione e 2) assicurerà che non ci sia overflow intero.
Charles Burns

(A + B) .GetHashCode () è un algoritmo di hashing pessimo, poiché diversi set di (A, B) possono dare lo stesso hash se sono concatenati alla stessa stringa; "hellow" + "ned" è uguale a "hell" + "own" e risulterebbe nello stesso hash.
kaesve

@kaesve che ne dici di (A + "" + B) .GetHashCode ()?
Timeless

1

E la Hashtablelezione!

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

Nel modo precedente, puoi usare qualsiasi oggetto (il tuo oggetto di classe) come chiave di dizionario generica :)


1

Ho avuto lo stesso problema. Ora posso utilizzare qualsiasi oggetto che ho provato come chiave a causa dell'override di Equals e GetHashCode.

Ecco una classe che ho costruito con metodi da utilizzare all'interno degli override di Equals (object obj) e GetHashCode (). Ho deciso di utilizzare generici e un algoritmo di hashing che dovrebbe essere in grado di coprire la maggior parte degli oggetti. Per favore fatemi sapere se vedete qualcosa qui che non funziona per alcuni tipi di oggetti e avete un modo per migliorarlo.

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

Ecco come viene utilizzato in una 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);
        }
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.