Множество HashSet
Представьте список гостей у входа на праздник. Охранник проверяет имя: если его в списке нет, вписывает, а если есть — второй раз не пишет. Сколько бы раз человек ни подходил к двери, его имя в списке одно.
Такой набор в .NET называется множеством: это набор значений, где каждое значение встречается не больше одного раза. Его тип — HashSet<T>. Любое значение либо есть в множестве, либо его там нет:
var guests = new HashSet<string>();
Console.WriteLine(guests.Add("Ann")); // True
Console.WriteLine(guests.Add("Bob")); // True
Console.WriteLine(guests.Add("Ann")); // False
Console.WriteLine(guests.Count); // 2
Console.WriteLine(guests.Contains("Bob")); // True
Console.WriteLine(guests.Remove("Bob")); // True
Console.WriteLine(guests.Contains("Bob")); // FalseДавайте разберём программу по строкам:
Add("Ann")добавляет имя и возвращаетtrue.Add("Bob")тоже добавляет и тоже возвращаетtrue.- Повторный
Add("Ann")ничего не добавляет и возвращаетfalse: имя уже есть. Countравен2:Annзаписана один раз.Contains("Bob")отвечает, есть ли значение:true.Remove("Bob")удаляет значение и возвращаетtrue, потому что оно было. После этогоContainsдаётfalse.
Сравните со словарём. Add словаря с уже существующим ключом останавливает программу исключением. А Add множества при повторе возвращает false и ничего не меняет.
Убрать повторы
Множество можно создать из списка или массива: new HashSet<int>(nums) берёт все элементы и оставляет каждое значение один раз. Подойдут и значения в квадратных скобках:
List<int> nums = [3, 1, 3, 2, 1];
var unique = new HashSet<int>(nums);
Console.WriteLine(nums.Count); // 5
Console.WriteLine(unique.Count); // 3
HashSet<string> tags = ["go", "cs", "go"];
Console.WriteLine(tags.Count); // 2Из пяти чисел остались три разных: 1, 2 и 3. Сам список nums не изменился: множество хранит свою копию значений. И в tags значение go попало только один раз.
Быстрая проверка «уже было?»
Contains у множества работает как поиск ключа в словаре: по хешу, без перебора. А Contains у списка проверяет элементы по одному. Поэтому на собеседовании задачу «найти первый повтор в массиве» решают множеством:
int[] ids = [4, 8, 15, 8, 16];
var seen = new HashSet<int>();
foreach (int id in ids)
{
if (!seen.Add(id))
{
Console.WriteLine(id); // 8
break;
}
}Разберём цикл:
seenхранит числа, которые уже встречались.seen.Add(id)добавляет новое число и возвращаетtrue.- Если число уже было,
Addвозвращаетfalse, и!превращает этотfalseвtrue. - Тогда цикл печатает первый повтор,
8, и выходит черезbreak.
Это один проход по массиву. Решение с двумя вложенными циклами сравнивало бы каждый элемент с каждым.
Порядка нет
У множества нет индексов, и порядок значений в нём не гарантирован: оно отвечает только на вопрос «есть или нет». Если важен порядок или нужны повторы, берите список.
Здесь листок охранника и множество расходятся. На листке имена записаны в том порядке, в каком приходили гости. Множество этот порядок не хранит: foreach по нему выдаёт значения в последовательности, на которую нельзя полагаться.
Частая ошибка
Значение множества попробовали прочитать по индексу:
var guests = new HashSet<string>();
guests.Add("Ann");
Console.WriteLine(guests[0]); // ✗error CS0021: Cannot apply indexing with [] to an expression of type 'HashSet<string>'Программа не компилируется: у HashSet нет индексатора, ведь у его значений нет индексов. Чтобы узнать, есть ли значение, вызывайте Contains. Если нужен элемент по индексу, храните значения в списке.
Главное
HashSet<T>хранит каждое значение один раз: повторныйAddвозвращаетfalseи ничего не добавляет.Containsу множества ищет по хешу, без перебора, поэтому множество подходит для проверки «уже встречалось?».new HashSet<int>(nums)оставляет из списка или массива только разные значения.- У множества нет индексов и гарантированного порядка:
guests[0]не компилируется.