Как HashSet сравнивает элементы на равенство?

128

У меня есть класс 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«s CompareToметод.
  2. Есть HashSetлучший способ получить список уникальных объектов?
Нима
источник
Добавьте реализацию IEqualityComparer<T>в конструктор или реализуйте ее в классе a. msdn.microsoft.com/en-us/library/bb301504(v=vs.110).aspx
Джайдер

Ответы:

138

Он использует 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>вместо 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 02 окт.13,
Imo, как только вы реализуете 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 01
1
@Simon_Weaver это так. Я действительно хочу как-то избежать этого в своей гипотетической функции, которую я предлагал.
nawfal 01
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)при необходимости при работе с набором.

tyriker
источник
2
Хорошая точка зрения. Кстати, как упоминалось в моем комментарии к ответу @JonSkeet, вы также должны реализовать bool IEquatable<T>.Equals(T other)для небольшого повышения эффективности, но, что более важно, преимущества ясности. По очевидным причинам, помимо необходимости реализации GetHashCodeпараллельно IEquatable<T>, в документе для IEquatable <T> упоминается, что для обеспечения согласованности вы также должны переопределить object.Equalsдля согласованности
Рубен Бартелинк
Я пробовал реализовать это. В ovveride getHashcodeработает, но override bool equalsполучает ошибку: ни один метод не нашел для переопределения. любая идея?
Stefanvds
Наконец то информация, которую я искал. Спасибо.
Mauro
Из моих комментариев к ответу выше - В вашем случае «Вместо» вы могли бы public class a : IEqualityComparer<a> {, а затем new HashSet<a>(a).
HankCa
Но см. Комментарии Джона Скитса выше.
HankCa
9

HashSetиспользует Equalsи GetHashCode().

CompareTo для заказанных наборов.

Если вам нужны уникальные объекты, но вам не важен порядок их итераций, HashSet<T>обычно это лучший выбор.

CodesInChaos
источник
5

Конструктор HashSet получает объект, который реализует IEqualityComparer для добавления нового объекта. если вы хотите использовать метод в HashSet, вам необходимо переопределить 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");
        }
    }
}
Николай Нечай
источник
Что делать, если имя не указано? какое хеш-значение у null?
Джо