In che modo HashSet confronta gli elementi per l'uguaglianza?


127

Ho una classe che è IComparable:

public class a : IComparable
{
    public int Id { get; set; }
    public string Name { get; set; }

    public a(int id)
    {
        this.Id = id;
    }

    public int CompareTo(object obj)
    {
        return this.Id.CompareTo(((a)obj).Id);
    }
}

Quando aggiungo un elenco di oggetti di questa classe a un set di hash:

a a1 = new a(1);
a a2 = new a(2);
HashSet<a> ha = new HashSet<a>();
ha.add(a1);
ha.add(a2);
ha.add(a1);

Va tutto bene ed ha.countè 2, ma:

a a1 = new a(1);
a a2 = new a(2);
HashSet<a> ha = new HashSet<a>();
ha.add(a1);
ha.add(a2);
ha.add(new a(1));

Adesso lo ha.countè 3.

  1. Perché non HashSetrispetta ail CompareTometodo.
  2. È HashSetil modo migliore per avere un elenco di oggetti unici?

Aggiungi un'implementazione di IEqualityComparer<T>nel costruttore o implementala nella classe a. msdn.microsoft.com/en-us/library/bb301504(v=vs.110).aspx
Jaider

Risposte:


137

Usa un IEqualityComparer<T>(a EqualityComparer<T>.Defaultmeno che non ne specifichi uno diverso in costruzione).

Quando aggiungi un elemento al set, troverà il codice hash usando IEqualityComparer<T>.GetHashCodee memorizzerà sia il codice hash che l'elemento (dopo aver verificato se l'elemento è già nel set, ovviamente).

Per cercare un elemento, utilizzerà prima il IEqualityComparer<T>.GetHashCodeper trovare il codice hash, quindi per tutti gli elementi con lo stesso codice hash, userà IEqualityComparer<T>.Equalsper confrontare per l'effettiva uguaglianza.

Ciò significa che hai due opzioni:

  • Passa un'abitudine IEqualityComparer<T>nel costruttore. Questa è l'opzione migliore se non puoi modificare la Tstessa, o se vuoi una relazione di uguaglianza non predefinita (es. "Tutti gli utenti con un ID utente negativo sono considerati uguali"). Questo non viene quasi mai implementato sul tipo stesso (ovvero Foonon implementato IEqualityComparer<Foo>) ma in un tipo separato che viene utilizzato solo per i confronti.
  • Implementare l'uguaglianza nel tipo stesso, sovrascrivendo GetHashCodee Equals(object). Idealmente, implementalo anche IEquatable<T>nel tipo, in particolare se si tratta di un tipo di valore. Questi metodi verranno chiamati dal comparatore di uguaglianza predefinito.

Nota come nulla di tutto ciò sia in termini di un confronto ordinato - il che ha senso, poiché ci sono certamente situazioni in cui puoi facilmente specificare l'uguaglianza ma non un ordine totale. Questo è lo stesso Dictionary<TKey, TValue>, in sostanza.

Se si desidera un set che utilizza l' ordinamento anziché solo confronti di uguaglianza, è necessario utilizzare SortedSet<T>da .NET 4, che consente di specificare un IComparer<T>anziché un IEqualityComparer<T>. Questo utilizzerà IComparer<T>.Compare- che delegherà a IComparable<T>.CompareToo IComparable.CompareTose si sta utilizzando Comparer<T>.Default.


7
+1 Nota anche la risposta di @ tyriker (che l'IMO dovrebbe essere un commento qui) che sottolinea che il modo più semplice per sfruttare detto IEqualityComparer<T>.GetHashCode/Equals()è implementare Equalse GetHashCodesu Tse stesso (e mentre lo stai facendo, implementeresti anche la controparte fortemente tipizzata : - bool IEquatable<T>.Equals(T other))
Ruben Bartelink

5
Anche se molto precisa questa risposta può essere un po 'confusa, specialmente per i nuovi utenti in quanto non afferma chiaramente che per il caso più semplice è prioritaria Equalsed GetHashCodeè sufficiente - come menzionato nella risposta di @ tyriker.
BartoszKP

Imo una volta implementato IComparable(o IComparerper quello) non dovresti chiedere di implementare l'uguaglianza separatamente (ma solo GetHashCode). In un certo senso le interfacce di comparabilità dovrebbero ereditare dalle interfacce di uguaglianza. Comprendo i vantaggi in termini di prestazioni nell'avere due funzioni separate (dove è possibile ottimizzare l'uguaglianza separatamente solo dicendo se qualcosa è uguale o meno) ma comunque .. Molto confuso altrimenti quando si è specificato quando le istanze sono uguali in CompareTofunzione e il framework non considererà quello.
nawfal,

@nawfal non tutto ha un ordine logico. se stai confrontando due cose che contengono una proprietà bool, è semplicemente terribile dover scrivere qualcosa di simile a.boolProp == b.boolProp ? 1 : 0o dovrebbe essere a.boolProp == b.boolProp ? 0 : -1o a.boolProp == b.boolProp ? 1 : -1. Che schiffo!
Simon_Weaver,

1
@Simon_Weaver lo è. Voglio in qualche modo evitarlo nella mia ipotetica caratteristica che stavo proponendo.
nawfal,

77

Ecco un chiarimento su una parte della risposta che è stata lasciata non detta: il tipo di oggetto HashSet<T>non deve essere implementato, IEqualityComparer<T>ma deve solo sostituire Object.GetHashCode()e Object.Equals(Object obj).

Invece di questo:

public class a : IEqualityComparer<a>
{
  public int GetHashCode(a obj) { /* Implementation */ }
  public bool Equals(a obj1, a obj2) { /* Implementation */ }
}

Tu lo fai:

public class a
{
  public override int GetHashCode() { /* Implementation */ }
  public override bool Equals(object obj) { /* Implementation */ }
}

È sottile, ma questo mi ha fatto scattare per la parte migliore di una giornata cercando di far funzionare HashSet nel modo previsto. E come altri hanno già detto, HashSet<a>finirà per chiamare a.GetHashCode()e a.Equals(obj)se necessario quando si lavora con il set.


2
Buon punto. A proposito, come menzionato nel mio commento sulla risposta di @ JonSkeet, dovresti anche implementare bool IEquatable<T>.Equals(T other)un leggero guadagno di efficienza, ma soprattutto il vantaggio di chiarezza. Per ragioni ovvie, oltre alla necessità di implementare a GetHashCodefianco IEquatable<T>, il documento per IEquatable <T> menziona che per motivi di coerenza dovresti anche ignorare la object.Equalsper coerenza
Ruben Bartelink,

Ho provato a implementarlo. I ovveride getHashcodelavori, ma override bool equalsottiene l'errore: nessun metodo trovato a override. qualche idea?
Stefanvds,

Finalmente le informazioni che stavo cercando. Grazie.
Mauro Sampietro,

Dai miei commenti sulla risposta sopra - Nel tuo caso "Invece di", potresti avere public class a : IEqualityComparer<a> {, e poi new HashSet<a>(a).
HankCa,

Ma vedi i commenti di Jon Skeets sopra.
HankCa,

9

HashSetusa Equalse GetHashCode().

CompareTo è per set ordinati.

Se desideri oggetti unici, ma non ti importa del loro ordine di iterazione, HashSet<T>è in genere la scelta migliore.


5

il costruttore HashSet riceve l'oggetto che implementa IEqualityComparer per l'aggiunta di un nuovo oggetto. se si desidera utilizzare il metodo in HashSet, non è necessario sostituire Equals, GetHashCode

namespace HashSet
{
    public class Employe
    {
        public Employe() {
        }

        public string Name { get; set; }

        public override string ToString()  {
            return Name;
        }

        public override bool Equals(object obj) {
            return this.Name.Equals(((Employe)obj).Name);
        }

        public override int GetHashCode() {
            return this.Name.GetHashCode();
        }
    }

    class EmployeComparer : IEqualityComparer<Employe>
    {
        public bool Equals(Employe x, Employe y)
        {
            return x.Name.Trim().ToLower().Equals(y.Name.Trim().ToLower());
        }

        public int GetHashCode(Employe obj)
        {
            return obj.Name.GetHashCode();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            HashSet<Employe> hashSet = new HashSet<Employe>(new EmployeComparer());
            hashSet.Add(new Employe() { Name = "Nik" });
            hashSet.Add(new Employe() { Name = "Rob" });
            hashSet.Add(new Employe() { Name = "Joe" });
            Display(hashSet);
            hashSet.Add(new Employe() { Name = "Rob" });
            Display(hashSet);

            HashSet<Employe> hashSetB = new HashSet<Employe>(new EmployeComparer());
            hashSetB.Add(new Employe() { Name = "Max" });
            hashSetB.Add(new Employe() { Name = "Solomon" });
            hashSetB.Add(new Employe() { Name = "Werter" });
            hashSetB.Add(new Employe() { Name = "Rob" });
            Display(hashSetB);

            var union = hashSet.Union<Employe>(hashSetB).ToList();
            Display(union);
            var inter = hashSet.Intersect<Employe>(hashSetB).ToList();
            Display(inter);
            var except = hashSet.Except<Employe>(hashSetB).ToList();
            Display(except);

            Console.ReadKey();
        }

        static void Display(HashSet<Employe> hashSet)
        {
            if (hashSet.Count == 0)
            {
                Console.Write("Collection is Empty");
                return;
            }
            foreach (var item in hashSet)
            {
                Console.Write("{0}, ", item);
            }
            Console.Write("\n");
        }

        static void Display(List<Employe> list)
        {
            if (list.Count == 0)
            {
                Console.WriteLine("Collection is Empty");
                return;
            }
            foreach (var item in list)
            {
                Console.Write("{0}, ", item);
            }
            Console.Write("\n");
        }
    }
}

Cosa succede se il nome è null? qual è il valore hash di null?
joe
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.