Як HashSet порівнює елементи рівності?


127

У мене клас 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);
    }
}

Коли я додаю список об’єктів цього класу до набору хешів:

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

Все добре і ha.countє 2, але:

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

Зараз ha.countє 3.

  1. Чому не HashSetповажає aросійський CompareToметод.
  2. Це HashSetнайкращий спосіб скласти список унікальних об’єктів?

Додайте реалізацію IEqualityComparer<T>в конструктор або реалізуйте її в класі a. msdn.microsoft.com/en-us/library/bb301504(v=vs.110).aspx
Jaider

Відповіді:


137

Він використовує IEqualityComparer<T>( EqualityComparer<T>.Defaultякщо ви не вказали інший на конструкції).

Коли ви додасте елемент до набору, він знайде хеш-код за допомогою IEqualityComparer<T>.GetHashCodeі збереже як хеш-код, так і елемент (після перевірки, чи елемент вже є у наборі, звичайно).

Щоб шукати елемент вгору, він спочатку використовуватиме IEqualityComparer<T>.GetHashCodeзнахідку хеш-коду, потім для всіх елементів з тим самим хеш-кодом він буде використовувати IEqualityComparer<T>.Equalsдля порівняння фактичної рівності.

Це означає, що у вас є два варіанти:

  • Передайте звичай IEqualityComparer<T>у конструктор. Це найкращий варіант, якщо ви не можете змінити Tсебе або якщо ви хочете відношення рівності за замовчуванням (наприклад, "всі користувачі з негативним ідентифікатором користувача вважаються рівними"). Це майже ніколи не реалізується на самому типі (тобто Fooне реалізує IEqualityComparer<Foo>), а в окремому типі, який використовується лише для порівняння.
  • Здійснюйте рівність у самому типі шляхом відміни GetHashCodeта Equals(object). В ідеалі також реалізовувати IEquatable<T>у тип, особливо якщо це тип значення. Ці методи будуть називатися порівнянням рівності за замовчуванням.

Зверніть увагу, як нічого з цього немає в плані упорядкованого порівняння - що має сенс, оскільки, безумовно, є ситуації, коли ви можете легко вказати рівність, але не загальне впорядкування. Це все те саме, що Dictionary<TKey, TValue>, в основному.

Якщо ви хочете набір, який використовує впорядкування замість просто порівняння рівності, вам слід скористатися SortedSet<T>з .NET 4 - що дозволяє вказати IComparer<T>замість an IEqualityComparer<T>. Для цього буде використано IComparer<T>.Compare- який буде делеговано до IComparable<T>.CompareToабо, IComparable.CompareToякщо ви використовуєте Comparer<T>.Default.


7
+1 Також зверніть увагу на відповідь @ tyriker (про те, що IMO має бути тут коментарем), який вказує, що найпростіший спосіб використовувати сказане IEqualityComparer<T>.GetHashCode/Equals()- це реалізовувати Equalsі GetHashCodeна Tсебе (і поки ви це робите, ви також будете реалізувати сильно набраний аналог : - bool IEquatable<T>.Equals(T other))
Рубен Бартелінк

5
Хоча дуже точний ця відповідь може бути кілька заплутаним, особливо для нових користувачів , так як це не ясно заявити , що в найпростішому випадку перекриває Equalsі GetHashCodeдосить - як згадано у відповіді @ tyriker в.
BartoszKP

Якщо ви реалізуєте IComparable(або IComparerз цього питання), вам не потрібно просити впроваджувати рівність окремо (а просто GetHashCode). У певному сенсі інтерфейси порівняння повинні успадковуватись від інтерфейсів рівності. Я розумію переваги продуктивності в двох окремих функцій (де ви можете оптимізувати рівність окремо , просто кажучи , якщо що - то одно чи ні) , але все ж .. Дуже заплутувати , коли ви визначили , коли екземпляри рівні CompareToфункції та структура звичай розглядати що.
nawfal

@nawfal не все має логічний порядок. якщо ви порівнюєте дві речі, які містять властивість bool, просто просто жахливо писати щось на кшталт a.boolProp == b.boolProp ? 1 : 0або повинно бути a.boolProp == b.boolProp ? 0 : -1або a.boolProp == b.boolProp ? 1 : -1. Юк!
Simon_Weaver

1
@Simon_Weaver це. Я хочу якось уникнути цього в своїй гіпотетичній особливості, яку я пропонував.
nawfal

77

Ось пояснення щодо частини відповіді, яка залишилася невимовленою: Тип об'єкта вашого HashSet<T>не потрібно реалізовувати, IEqualityComparer<T>а натомість просто повинен переосмислити Object.GetHashCode()таObject.Equals(Object obj) .

Замість цього:

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

Ви робите це:

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

Це тонко, але це спричинило мене протягом більшої частини дня, намагаючись змусити HashSet функціонувати так, як це було призначено. І як сказали інші, HashSet<a>закінчуватимете дзвінки a.GetHashCode()та a.Equals(obj)в міру необхідності під час роботи з набором.


2
Гарна думка. До речі, як згадувалося в моєму коментарі до відповіді @ JonSkeet, ви також повинні впроваджувати bool IEquatable<T>.Equals(T other)для невеликого підвищення ефективності, але що важливіше, ясність. З незрозумілих причин, окрім необхідності впровадження GetHashCodeразом із цим IEquatable<T>, документ для IEquatable <T> зазначає, що для цілей узгодженості слід також перекрити object.Equalsконсистенцію
Рубен Бартелінк

Я спробував це здійснити. У ovveride getHashcodeпрацює, але override bool equalsотримує помилку: ні один метод не знайшов для перевизначення. будь-яка ідея?
Штефавдс

Нарешті інформацію, яку я шукав. Дякую.
Мауро Самп'єтро

З моїх коментарів до вищезгаданої відповіді - У вашому випадку "Замість" ви могли б мати public class a : IEqualityComparer<a> {і тоді new HashSet<a>(a).
HankCa

Але дивіться коментарі Джона Скітса вище.
HankCa

9

HashSetвикористовує Equalsі GetHashCode().

CompareTo призначений для замовлених наборів.

Якщо ви хочете унікальних об'єктів, але вам не байдуже їх порядок ітерації, HashSet<T>як правило, найкращий вибір.


5

конструктор HashSet отримує об'єкт, що реалізує IEqualityComparer для додавання нового об'єкта. якщо ви не хочете використовувати метод в HashSet, ви не зможете переотримати значення рівних, 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");
        }
    }
}

Що робити, якщо ім'я недійсне? яке хеш-значення null?
Джо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.