Un HashSet<T> è una collezione in cui ogni elemento compare al massimo una volta. Non ha un indice né un ordine garantito, e in cambio verifica l'appartenenza in un tempo più o meno costante: trovare un tag tra un milione richiede all'incirca lo stesso tempo che trovarlo tra dieci.
Aggiungere elementi: Add restituisce bool
Output:
True
False
3
True
True
False
2
Aggiungere un duplicato non è un errore; Add restituisce semplicemente false e il set resta invariato. Quel valore di ritorno è la cosa più utile del metodo. Unisce "l'ho già visto?" e "ricordatelo" in una sola chiamata:
Output:
Duplicate: ana@x.com
Duplicate: ben@x.com
3 unique
Perché Contains è veloce
List<T>.Contains confronta il valore con ogni elemento a turno, quindi il suo costo cresce con la lista. Un HashSet<T> calcola l'hash code dell'elemento, salta al bucket di quel codice e confronta solo i pochi elementi che vi sono memorizzati. Per un test di appartenenza dentro un ciclo, questo trasforma un passo O(n) in un passo O(1), e un ciclo annidato su due liste in un unico passaggio:
// 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));
Costruire il set costa un passaggio sulla lista, quindi conviene solo quando fai più di qualche ricerca.
Rimuovere i duplicati da una lista
Ci sono tre modi comuni, e differiscono per cosa succede all'ordine:
Output:
Lima, Oslo, Pune, Kyiv
4
Lima, Oslo, Pune, Kyiv
Distinct è la scelta predefinita giusta quando vuoi ottenere una lista. La versione sul posto funziona perché RemoveAll chiama il predicato una volta per elemento, in ordine: seen.Add restituisce false per la seconda copia e le successive, quindi vengono rimosse esattamente quelle.
Operazioni sugli insiemi: unione, intersezione, differenza
HashSet<T> ha le operazioni della teoria degli insiemi. I metodi ...With modificano il set su cui vengono chiamati e non restituiscono nulla.
Output:
Union: Ana, Ben, Chloe, Dev
Intersect: Ben, Chloe
Except: Ana
Symmetric: Ana, Dev
True
True
False
True
L'argomento può essere qualsiasi IEnumerable<T>: un array, una lista o un altro set. SetEquals ignora l'ordine e i duplicati nell'argomento. L'helper Show ordina prima di stampare perché l'ordine di enumerazione di un set non è qualcosa su cui fare affidamento.
LINQ ha metodi corrispondenti che restituiscono una nuova sequenza e lasciano invariati gli input: monday.Union(tuesday), monday.Intersect(tuesday), monday.Except(tuesday). Usali quando non vuoi modificare un set, o quando gli input sono liste.
Uguaglianza personalizzata per le tue classi
Un set decide se due elementi sono "lo stesso" con GetHashCode ed Equals. Per una classe che non li sovrascrive, entrambi si basano sull'identità dell'oggetto, quindi due oggetti con campi uguali sono due elementi diversi:
Output:
2
1
True
La regola: oggetti che sono Equals devono restituire lo stesso GetHashCode. Se sovrascrivi solo Equals, il set cerca nel bucket sbagliato e segnala ancora duplicati. Su .NET Core 2.1 e successivi, HashCode.Combine(X, Y) costruisce un buon hash code senza l'aritmetica scritta a mano.
Quando non puoi modificare la classe, o ti serve una nozione diversa di "stesso" per un solo set, passa un IEqualityComparer<T> al costruttore. Le stringhe hanno comparer già pronti:
Output:
True
False
2
In C# 9 e successivi, un record genera per te Equals e GetHashCode basati sul valore, quindi record Point(int X, int Y); funziona in un set senza codice aggiuntivo.
Non cambiare mai un campo che contribuisce a GetHashCode mentre l'oggetto è in un set. L'oggetto resta nel bucket del suo vecchio hash, quindi Contains e Remove non lo trovano più.
Ordine e SortedSet
Un HashSet<T> enumera in un ordine che devi considerare arbitrario. Se ti servono gli elementi ordinati, ordinali quando stampi (set.OrderBy(x => x)) oppure usa SortedSet<T>, che mantiene sempre gli elementi in ordine e aggiunge Min, Max e query per intervallo a O(log n) per operazione:
var ranks = new SortedSet<int> { 30, 10, 20 };
Console.WriteLine(string.Join(", ", ranks)); // 10, 20, 30
Console.WriteLine(ranks.Min); // 10
HashSet, List o Dictionary
| Esigenza | Usa |
|---|---|
| Elementi unici, "c'è?" veloce | HashSet<T> |
| Elementi unici, sempre ordinati | SortedSet<T> |
| Ordine, duplicati, accesso per indice | List<T> |
| Un valore memorizzato sotto ogni chiave | Dictionary<TKey, TValue> |
Un set è un dizionario con le chiavi e senza valori. Se ti ritrovi a scrivere Dictionary<string, bool> solo per tenere traccia dell'appartenenza, un HashSet<string> dice la stessa cosa in modo più chiaro.
Errori comuni
- Aspettarsi un ordine. Un set non ne ha uno su cui contare; ordina, oppure usa
SortedSet<T>. - Classi personalizzate senza
EqualseGetHashCode. Oggetti che sembrano uguali diventano elementi separati. - Sovrascrivere solo
Equals. Sovrascrivi sempre ancheGetHashCode. - Modificare un elemento dopo averlo aggiunto. Il set non riesce più a trovarlo.
- Accedere a un set per indice.
set[0]non compila; non c'è un indice. Converti conToList()se ti servono le posizioni.
Domande frequenti
Che cos'è un HashSet in C#?
HashSet<T> è una collezione di elementi unici senza un ordine definito. Aggiungere un elemento già presente non fa nulla, e Contains risponde in un tempo più o meno costante, per quanto grande sia il set, perché gli elementi sono memorizzati per hash code come le chiavi di un Dictionary.
Cosa restituisce HashSet.Add?
Add restituisce true quando l'elemento è stato aggiunto e false quando era già nel set. Questo rende if (!seen.Add(x)) un controllo dei duplicati in una riga: aggiunge gli elementi nuovi e ti segnala quelli ripetuti con la stessa chiamata.
Come rimuovo i duplicati da una List in C#?
list.Distinct().ToList() restituisce una nuova lista senza duplicati e conserva la prima occorrenza di ogni elemento nel suo ordine originale. Anche new HashSet<T>(list) rimuove i duplicati, ma un set non ha un ordine garantito. Per eliminare i duplicati sul posto, usa var seen = new HashSet<T>(); list.RemoveAll(x => !seen.Add(x));.
Quando conviene usare un HashSet invece di una List?
Usa un HashSet<T> quando chiedi soprattutto "questo elemento è nella collezione?" o quando gli elementi devono essere unici. List<T>.Contains scorre ogni elemento, quindi rallenta man mano che la lista cresce, mentre HashSet<T>.Contains no. Usa una List<T> quando contano l'ordine, i duplicati o l'accesso per indice.
Perché il mio HashSet contiene oggetti duplicati?
La tua classe non sovrascrive Equals e GetHashCode, quindi il set confronta i riferimenti e due oggetti con gli stessi valori nei campi risultano diversi. Sovrascrivi entrambi i metodi (sempre insieme), oppure passa un IEqualityComparer<T> al costruttore del set.