HashSet<T> to kolekcja, w której każdy element występuje najwyżej raz. Nie ma indeksu ani gwarantowanej kolejności, a w zamian sprawdza przynależność w mniej więcej stałym czasie: znalezienie jednego tagu wśród miliona trwa mniej więcej tyle samo, co wśród dziesięciu.
Dodawanie elementów: Add zwraca bool
Wynik:
True
False
3
True
True
False
2
Dodanie duplikatu nie jest błędem; Add po prostu zwraca false, a zbiór się nie zmienia. Ta zwracana wartość to najbardziej przydatna cecha tej metody. Łączy "czy to już było?" i "zapamiętaj to" w jedno wywołanie:
Wynik:
Duplicate: ana@x.com
Duplicate: ben@x.com
3 unique
Dlaczego Contains jest szybkie
List<T>.Contains porównuje wartość po kolei z każdym elementem, więc koszt rośnie wraz z listą. HashSet<T> oblicza kod skrótu elementu, przeskakuje do kubełka dla tego kodu i porównuje tylko kilka przechowywanych tam elementów. Przy sprawdzaniu przynależności w pętli zamienia to krok O(n) w krok O(1), a zagnieżdżoną pętlę po dwóch listach w jedno przejście:
// Slow on large inputs: Contains scans bannedList for every order.
var flagged = orders.Where(o => bannedList.Contains(o.CustomerId));
// Fast: build the set once, then each lookup is constant time.
var banned = new HashSet<int>(bannedList);
var flagged2 = orders.Where(o => banned.Contains(o.CustomerId));
Zbudowanie zbioru kosztuje jedno przejście po liście, więc opłaca się dopiero wtedy, gdy wyszukujesz więcej niż kilka razy.
Usuwanie duplikatów z listy
Są trzy typowe sposoby i różnią się tym, co dzieje się z kolejnością:
Wynik:
Lima, Oslo, Pune, Kyiv
4
Lima, Oslo, Pune, Kyiv
Distinct to właściwy wybór domyślny, gdy chcesz z powrotem listę. Wersja w miejscu działa, bo RemoveAll wywołuje predykat raz dla każdego elementu, po kolei: seen.Add zwraca false dla drugiej i kolejnych kopii, więc usuwane są dokładnie one.
Operacje na zbiorach: suma, część wspólna, różnica
HashSet<T> ma operacje znane z teorii zbiorów. Metody ...With zmieniają zbiór, na którym je wywołano, i nic nie zwracają.
Wynik:
Union: Ana, Ben, Chloe, Dev
Intersect: Ben, Chloe
Except: Ana
Symmetric: Ana, Dev
True
True
False
True
Argumentem może być dowolny IEnumerable<T>: tablica, lista albo inny zbiór. SetEquals ignoruje kolejność i duplikaty w argumencie. Pomocnik Show sortuje przed wypisaniem, bo na kolejności wyliczania zbioru nie należy polegać.
LINQ ma odpowiadające metody, które zwracają nową sekwencję i nie ruszają danych wejściowych: monday.Union(tuesday), monday.Intersect(tuesday), monday.Except(tuesday). Używaj ich, gdy nie chcesz modyfikować zbioru albo gdy dane wejściowe to listy.
Własna równość dla twoich klas
Zbiór rozstrzyga, czy to "ten sam element", przez GetHashCode i Equals. W klasie, która ich nie nadpisuje, obie opierają się na tożsamości obiektu, więc dwa obiekty o równych polach to dwa różne elementy:
Wynik:
2
1
True
Zasada: obiekty, które są równe według Equals, muszą zwracać ten sam GetHashCode. Jeśli nadpiszesz tylko Equals, zbiór szuka w złym kubełku i nadal zgłasza duplikaty. W .NET Core 2.1 i nowszych HashCode.Combine(X, Y) buduje dobry kod skrótu bez ręcznej arytmetyki.
Gdy nie możesz zmienić klasy albo potrzebujesz innego pojęcia "tego samego" dla jednego zbioru, przekaż IEqualityComparer<T> do konstruktora. Stringi mają gotowe comparery:
Wynik:
True
False
2
W C# 9 i nowszych record generuje za ciebie Equals i GetHashCode oparte na wartościach, więc record Point(int X, int Y); działa w zbiorze bez dodatkowego kodu.
Nigdy nie zmieniaj pola, z którego korzysta GetHashCode, gdy obiekt jest w zbiorze. Obiekt zostaje w kubełku dla starego kodu skrótu, więc Contains i Remove przestają go znajdować.
Kolejność i SortedSet
HashSet<T> wylicza elementy w kolejności, którą należy traktować jako przypadkową. Jeśli potrzebujesz posortowanych elementów, sortuj przy wypisywaniu (set.OrderBy(x => x)) albo użyj SortedSet<T>, który cały czas trzyma elementy w porządku i dodaje Min, Max oraz zapytania o zakres, w czasie O(log n) na operację:
var ranks = new SortedSet<int> { 30, 10, 20 };
Console.WriteLine(string.Join(", ", ranks)); // 10, 20, 30
Console.WriteLine(ranks.Min); // 10
HashSet a List a Dictionary
| Potrzeba | Użyj |
|---|---|
| Unikalne elementy, szybkie "czy jest?" | HashSet<T> |
| Unikalne elementy, zawsze posortowane | SortedSet<T> |
| Kolejność, duplikaty, dostęp przez indeks | List<T> |
| Wartość przechowywana pod każdym kluczem | Dictionary<TKey, TValue> |
Zbiór to słownik z kluczami bez wartości. Jeśli piszesz Dictionary<string, bool> tylko po to, żeby śledzić przynależność, HashSet<string> wyraża to samo czytelniej.
Typowe błędy
- Oczekiwanie kolejności. Zbiór nie ma kolejności, na której można polegać; sortuj albo użyj
SortedSet<T>. - Własne klasy bez
EqualsiGetHashCode. Obiekty, które wyglądają tak samo, stają się osobnymi elementami. - Nadpisanie samego
Equals. Zawsze nadpisuj razem z nimGetHashCode. - Zmiana elementu po dodaniu go. Zbiór nie może go już znaleźć.
- Indeksowanie zbioru.
set[0]się nie kompiluje; nie ma indeksu. Jeśli potrzebujesz pozycji, skonwertuj przezToList().
Najczęściej zadawane pytania
Czym jest HashSet w C#?
HashSet<T> to kolekcja unikalnych elementów bez określonej kolejności. Dodanie elementu, który już jest w zbiorze, nic nie robi, a Contains odpowiada w mniej więcej stałym czasie niezależnie od rozmiaru zbioru, bo elementy są przechowywane według kodu skrótu, tak jak klucze w Dictionary.
Co zwraca HashSet.Add?
Add zwraca true, gdy element został dodany, i false, gdy już był w zbiorze. Dzięki temu if (!seen.Add(x)) to jednolinijkowe sprawdzenie duplikatów: w jednym wywołaniu dodaje nowe elementy i informuje o powtórzonych.
Jak usunąć duplikaty z List w C#?
list.Distinct().ToList() zwraca nową listę bez duplikatów i zachowuje pierwsze wystąpienie każdego elementu w pierwotnej kolejności. new HashSet<T>(list) też usuwa duplikaty, ale zbiór nie ma gwarantowanej kolejności. Aby usunąć duplikaty w miejscu, użyj var seen = new HashSet<T>(); list.RemoveAll(x => !seen.Add(x));.
Kiedy użyć HashSet zamiast List?
Użyj HashSet<T>, gdy najczęściej pytasz "czy ten element jest w kolekcji?" albo potrzebujesz, by elementy były unikalne. List<T>.Contains przegląda każdy element, więc zwalnia wraz ze wzrostem listy, a HashSet<T>.Contains nie. Użyj List<T>, gdy liczy się kolejność, duplikaty lub dostęp przez indeks.
Dlaczego mój HashSet zawiera zduplikowane obiekty?
Twoja klasa nie nadpisuje Equals i GetHashCode, więc zbiór porównuje referencje, a dwa obiekty o tych samych wartościach pól liczą się jako różne. Nadpisz obie metody (zawsze razem) albo przekaż IEqualityComparer<T> do konstruktora zbioru.