Jak HashSet porównuje elementy pod kątem równości?

127

Mam klasę, która jest 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);
    }
}

Kiedy dodam listę obiektów tej klasy do zestawu skrótów:

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

Wszystko jest w porządku i ha.countjest 2, ale:

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

Teraz ha.countjest 3.

  1. Dlaczego nie HashSetszanować a„s CompareTometody.
  2. Czy HashSetnajlepiej jest mieć listę unikalnych obiektów?
nima
źródło
Dodaj implementację IEqualityComparer<T>w konstruktorze lub zaimplementuj ją w klasie a. msdn.microsoft.com/en-us/library/bb301504(v=vs.110).aspx
Jaider

Odpowiedzi:

137

Używa IEqualityComparer<T>( EqualityComparer<T>.Defaultchyba że określisz inny w konstrukcji).

Kiedy dodasz element do zestawu, znajdzie on kod skrótu za pomocą IEqualityComparer<T>.GetHashCodei zapisze zarówno kod skrótu, jak i element (oczywiście po sprawdzeniu, czy element jest już w zestawie).

Aby wyszukać element, najpierw użyje on IEqualityComparer<T>.GetHashCodedo znalezienia kodu skrótu, a następnie dla wszystkich elementów z tym samym kodem skrótu użyje IEqualityComparer<T>.Equalsdo porównania rzeczywistej równości.

Oznacza to, że masz dwie możliwości:

  • Przekaż niestandardowy IEqualityComparer<T>konstruktor. Jest to najlepsza opcja, jeśli nie możesz zmodyfikować Tsamego siebie lub jeśli chcesz mieć inną niż domyślną relację równości (np. „Wszyscy użytkownicy z ujemnym identyfikatorem użytkownika są traktowani jako równi”). Prawie nigdy nie jest to implementowane w samym typie (tj. FooNie implementuje IEqualityComparer<Foo>), ale w osobnym typie, który jest używany tylko do porównań.
  • Zaimplementuj równość w samym typie, zastępując GetHashCodei Equals(object). W idealnym przypadku zaimplementuj również IEquatable<T>w typie, szczególnie jeśli jest to typ wartości. Te metody będą wywoływane przez domyślną funkcję porównującą równość.

Zwróć uwagę, że nic z tego nie dotyczy uporządkowanego porównania - co ma sens, ponieważ z pewnością są sytuacje, w których można łatwo określić równość, ale nie całkowitą kolejność. To wszystko jest takie samo jak Dictionary<TKey, TValue>w zasadzie.

Jeśli chcesz zestaw, który używa porządkowania zamiast tylko porównań równości, powinieneś użyć SortedSet<T>z .NET 4 - co pozwala określić IComparer<T>zamiast IEqualityComparer<T>. Spowoduje to użycie IComparer<T>.Compare- które będzie delegować do IComparable<T>.CompareTolub IComparable.CompareTojeśli używasz Comparer<T>.Default.

Jon Skeet
źródło
7
+1 Zwróć także uwagę na odpowiedź @ tyrikera (że IMO powinno być tutaj komentarzem), która wskazuje, że najprostszym sposobem wykorzystania wspomnianej dźwigni IEqualityComparer<T>.GetHashCode/Equals()jest wdrożenie Equalsi GetHashCodena Tsobie (a robiąc to, zaimplementowałbyś również silnie wpisany odpowiednik : - bool IEquatable<T>.Equals(T other))
Ruben Bartelink
5
Chociaż bardzo dokładna odpowiedź ta może być nieco mylące, zwłaszcza dla nowych użytkowników, ponieważ nie jasno powiedzieć, że w najprostszym przypadku unieważniając Equalsi GetHashCodewystarczy - jak wspomniano w odpowiedzi @ tyriker użytkownika.
BartoszKP
Imo po wdrożeniu IComparable(lub jeśli IComparero to chodzi) nie powinieneś być proszony o wprowadzenie równości osobno (ale tylko GetHashCode). W pewnym sensie interfejsy porównywalności powinny dziedziczyć po interfejsach równości. Rozumiem korzyści wydajnościowe wynikające z posiadania dwóch oddzielnych funkcji (gdzie można optymalizować równość osobno, mówiąc tylko, czy coś jest równe, czy nie), ale nadal ... Bardzo mylące, jeśli określono, kiedy wystąpienia są równe pod względem CompareTofunkcji i struktury, których nie będzie rozważać że.
nawfal
@nawfal nie wszystko ma logiczną kolejność. jeśli porównujesz dwie rzeczy, które zawierają właściwość bool, po prostu okropne jest napisanie czegoś takiego, a.boolProp == b.boolProp ? 1 : 0czy powinno być a.boolProp == b.boolProp ? 0 : -1lub a.boolProp == b.boolProp ? 1 : -1. Fuj!
Simon_Weaver
1
@Simon_Weaver to jest. Chciałbym jakoś tego uniknąć w mojej hipotetycznej funkcji, którą proponowałem.
nawfal
77

Oto wyjaśnienie części odpowiedzi, która została niewypowiedziana: Typ obiektu HashSet<T>nie musi być implementowany, IEqualityComparer<T>ale zamiast tego musi nadpisać Object.GetHashCode()i Object.Equals(Object obj).

Zamiast tego:

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

Robisz to:

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

Jest to subtelne, ale przez większą część dnia denerwowało mnie, próbując sprawić, by HashSet działał tak, jak powinien. I jak powiedzieli inni, w HashSet<a>końcu zadzwoni a.GetHashCode()i a.Equals(obj)w razie potrzeby podczas pracy z zestawem.

tyriker
źródło
2
Słuszna uwaga. BTW, jak wspomniano w moim komentarzu do odpowiedzi @ JonSkeet, należy również wdrożyć, bool IEquatable<T>.Equals(T other)aby uzyskać niewielki wzrost wydajności, ale co ważniejsze, korzyść z przejrzystości. Z powodów obv, oprócz konieczności implementacji GetHashCodeobok IEquatable<T>, dokument dla IEquatable <T> wspomina, że ​​dla celów spójności należy również zastąpić object.Equalsspójność
Ruben Bartelink
Próbowałem to zaimplementować. Że ovveride getHashcodedziała, ale override bool equalsdostaje błąd: nie znaleziono sposób, aby zastąpić. dowolny pomysł?
Stefanvds
Wreszcie informacje, których szukałem. Dziękuję Ci.
Mauro Sampietro
Z moich komentarzy do powyższej odpowiedzi - W Twoim przypadku „Zamiast” mógłbyś mieć public class a : IEqualityComparer<a> {, a potem new HashSet<a>(a).
HankCa
Ale zobacz komentarze Jona Skeetsa powyżej.
HankCa,
9

HashSetużywa Equalsi GetHashCode().

CompareTo dotyczy zestawów zamówionych.

Jeśli chcesz unikalnych obiektów, ale nie zależy Ci na ich kolejności iteracji, HashSet<T>jest to zazwyczaj najlepszy wybór.

CodesInChaos
źródło
5

konstruktor HashSet otrzyma obiekt, który implementuje IEqualityComparer do dodawania nowego obiektu. jeśli chcesz użyć metody w HashSet, nie możesz zastąpić 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");
        }
    }
}
Nikolai Nechai
źródło
Co jeśli nazwa jest pusta? jaka jest wartość skrótu null?
joe