HashSet позволяет вставлять повторяющиеся элементы — C#

Этот вопрос кажется нубским, но я не смог найти ответ конкретно на этот вопрос.

У меня есть этот класс:

public class Quotes{ 
    public string symbol; 
    public string extension
}

И я использую это:

HashSet<Quotes> values = new HashSet<Quotes>();

Однако я могу добавлять один и тот же объект Quotes несколько раз. Например, мой объект Quotes может иметь «символ», равный «A», и «расширение», равное «= n», и этот объект Quotes появляется несколько раз в HashSet (просматривая Hashset в режиме отладки). Я думал, что когда звонил

values.Add(new Quotes(symb, ext));

с тем же symb и ext будет возвращено «false», и элемент не будет добавлен. У меня есть ощущение, что это как-то связано со сравнением объектов Quotes, когда HashSet добавляет новый объект. Любая помощь будет принята с благодарностью!


person jpints14    schedule 05.01.2012    source источник
comment
Возможно, вы захотите взглянуть на HashTable или, что еще лучше, на Dictionary‹string,sting›   -  person MethodMan    schedule 05.01.2012
comment
@ jpints14 на что ты хэшируешь? содержимое строки или место в памяти? (или другой)   -  person Adrian    schedule 05.01.2012
comment
Под возможностью добавлять один и тот же объект Quotes несколько раз вы имеете в виду добавление одного и того же экземпляра или добавление идентичных экземпляров?   -  person James Michael Hare    schedule 05.01.2012


Ответы (7)


Я предполагаю, что вы создаете новый Quotes с теми же значениями. В данном случае они не равны. Если их следует считать равными, переопределите методы Equals и GetHashCode.

public class Quotes{ 
    public string symbol; 
    public string extension

    public override bool Equals(object obj)
    {
        Quotes q = obj as Quotes;
        return q != null && q.symbol == this.symbol && q.extension == this.Extension;
    }

    public override int GetHashCode()
    {
        return this.symbol.GetHashCode() ^ this.extension.GetHashCode();
    }
}
person Kendall Frey    schedule 05.01.2012
comment
Обратите внимание, что если символ или расширение могут быть нулевыми, то GetHashCode должен обработать это и не дать сбой. - person Eric Lippert; 06.01.2012
comment
У меня есть проверка, прежде чем сравнение когда-либо понадобится, но спасибо за совет - person jpints14; 06.01.2012
comment
Обратите внимание, что для типов полей, отличных от strings, ints или других типов значений или закрытых классов, вы должны использовать q != null && q.symbol.Equals(this.symbol) && q.extension.Equals(this.extension) вместо использования ==, потому что == не является полиморфным (т. е. если подклассы определяют operator ==, базовый класс 'orperator == все равно будет использоваться, в то время как подклассы могут переопределять метод .Equals(), поэтому будет использоваться подкласс .Equals(). Кроме того, hash1 ^ hash2 является плохой реализацией хеш-функции, поскольку "a", "b" и "b", "a" имеют одинаковый хеш. Предпочитайте что-то вроде (hash1 + 7 * 13) ^ hash2. - person Suzanne Soy; 20.09.2013

Я думал, что при вызове values.Add(new Quotes(symb, ext)); с тем же symb и ext будет возвращено «false» и элемент не будет добавлен.

Это не тот случай.

HashSet будет использовать GetHashCode и Equals для определения равенства ваших объектов. Прямо сейчас, поскольку вы не переопределяете эти методы в Quotes, будет использоваться равенство ссылок по умолчанию System.Object. Каждый раз, когда вы добавляете новый Quote, это уникальный экземпляр объекта, поэтому HashSet видит его как уникальный объект.

Если вы переопределите Object.Equals и Object.GetHashCode, все будет работать так, как вы ожидаете.

person Reed Copsey    schedule 05.01.2012

HashSets сначала сравнивает записи на основе их хэша, который рассчитывается с помощью GetHashCode.
Реализация по умолчанию возвращает хэш-код на основе самого объекта (различается для каждого экземпляра).

Только если хэши совпадают (что очень маловероятно для хэшей на основе экземпляров), вызывается метод Equals и используется для однозначного сравнения двух объектов.

У вас есть варианты:

  • Изменить кавычки на структуру
  • Переопределить GetHashCode и Equals в кавычках

Пример:

 public override int GetHashCode()
 {
    return (this.symbol == null ? 0 : this.symbol.GetHashCode())
       ^ (this.extension == null ? 0 : this.extension.GetHashCode());
 }
 public override bool Equals(object obj)
 {
    if (Object.ReferenceEquals(this, obj))
      return true;

    Quotes other = obj as Quotes;
    if (Object.ReferenceEquals(other, null))
      return false;

    return String.Equals(obj.symbol, this.symbol)
        && String.Equals(obj.extension, this.extension);
 }
person Matthias    schedule 05.01.2012
comment
Вам также необходимо переопределить Object.Equals - Уникальность хэшей не гарантируется, поэтому используются оба метода... - person Reed Copsey; 05.01.2012
comment
Да, слишком много внимания уделял написанию ответа достаточно быстро :-D Я только что добавил его, спасибо. - person Matthias; 05.01.2012
comment
ммм - я не думаю, что ваша проверка Object.ReferenceEquals совершенно правильна... ;) В основном, как у вас есть, каждый раз, когда obj является объектом Quotes, вы говорите, что он не равен (это единственный способ, которым он когда-либо может быть равным...) - person Reed Copsey; 05.01.2012
comment
Арх! Такое бывает, когда при наборе текста два if становятся одним... Кажется, мне пора сделать перерыв :-) - person Matthias; 05.01.2012
comment
hash1 ^ hash2 — это плохая реализация хэша, так как "a", "b" и "b", "a" имеют один и тот же хэш. Рассмотрим что-то вроде (hash1 + 7 * 13) ^ hash2. - person ErikE; 04.08.2015

Просто хотел что-то исправить в ответе Кендалла (не могу комментировать по какой-то странной причине).

return this.symbol.GetHashCode() ^ this.extension.GetHashCode();

Обратите внимание, что функция xor является исключительно подверженным коллизиям способом объединения двух хэшей, особенно когда они оба одного типа (поскольку каждый объект, где расширение symbol == будет хэшироваться в 0). Даже если они не одного типа или вряд ли будут равны друг другу, это плохая практика, и привыкание к ней может вызвать проблемы в разных приборах.

Вместо этого умножьте один хэш на небольшое простое число и добавьте второй, например:

return 3 * this.symbol.GetHashCode() + this.extension.GetHashCode();
person leetrobot    schedule 29.05.2015

Я знаю, что это немного поздно, но я столкнулся с той же проблемой и обнаружил неприемлемое снижение производительности при реализации выбранного ответа, особенно когда у вас много записей.

Я обнаружил, что гораздо быстрее превратить это в двухэтапный процесс с использованием Hashset и Tuple и, наконец, преобразовать с помощью Select.

public class Quotes{ 
    public string symbol; 
    public string extension
}

var values = new HashSet<Tuple<string,string>>();

values.Add(new Tuple<string,string>("A","=n"));
values.Add(new Tuple<string,string>("A","=n"));

// values.Count() == 1

values.Select (v => new Quotes{ symbol = v.Item1, extension = v.Item2 });
person user1265146    schedule 07.03.2015
comment
Попробуйте сравнить его с таким подходом, как принятый ответ, но также с Quotes реализацией IEquatable<Quotes>, и вы можете получить лучшие результаты. Лучшие результаты, вероятно, возможны при дальнейшей настройке GetHashCode(). - person Jon Hanna; 07.03.2015

Quotes q = new Quotes() { symbol = "GE", extension = "GElec" };
values.Add(q);
values.Add(q);

.. дважды добавляет один и тот же экземпляр и возвращает false во второй раз.

values.Add(new Quotes() { symbol = "GE", extension = "GElec" });
values.Add(new Quotes() { symbol = "GE", extension = "GElec" });

.. добавляет два разных экземпляра, которые имеют одинаковые значения для общедоступных полей.

Как отмечалось в другом месте, переопределение Equals и GetHashCode исправит это:

public class Quotes { 
    public string symbol; 
    public string extension;

    public override bool Equals(object obj) {
        if (!(obj is Quotes)) { return false; }
        return (this.symbol == ((Quotes)obj).symbol) && 
               (this.extension == ((Quotes)obj).extension);
    }

    public override int GetHashCode() {
        return (this.symbol.GetHashCode()) ^ (this.extension.GetHashCode());
    }
} 

Если вы выполните пошаговую отладку своего кода, вы обнаружите, что values.Add вызывает как Quotes.Equals, так и Quotes.GetHashCode.

person Joshua Honig    schedule 05.01.2012
comment
Что ^ делает в вашем return (this.symbol.GetHashCode()) ^ (this.extension.GetHashCode()); ? первый раз такое вижу это опечатка? - person Niklas; 17.10.2016

Мне сообщили, что переопределение Equals() и GetHashCode() не является хорошей практикой.

Классы — это ссылочные типы, а структуры — это типы значений. Переход к структуре позволит выполнить сравнение на равенство по значению, тем самым отображая идентичные символы/расширения как равные.

public struct Quotes { 
    public string symbol; 
    public string extension;
}

public static void Main()
{
    var hashSet = new HashSet<Quotes>();

    hashSet.Add(new Quotes { symbol = "aaa", extension = "bbb" });
    hashSet.Add(new Quotes { symbol = "aaa", extension = "bbb" });

    Console.WriteLine(hashSet.Count);
}

Выход 1.

person tibbiustin    schedule 03.04.2020