Menu

HashSet in C#: elementi unici, Contains e operazioni sugli insiemi

HashSet<T> contiene elementi unici e risponde a Contains in tempo costante. Scopri come Add segnala i duplicati, come rimuovere i duplicati da una lista, come fare unione, intersezione e differenza di insiemi, e come far confrontare a un set i tuoi oggetti per valore.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

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

EsigenzaUsa
Elementi unici, "c'è?" veloceHashSet<T>
Elementi unici, sempre ordinatiSortedSet<T>
Ordine, duplicati, accesso per indiceList<T>
Un valore memorizzato sotto ogni chiaveDictionary<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 Equals e GetHashCode. Oggetti che sembrano uguali diventano elementi separati.
  • Sovrascrivere solo Equals. Sovrascrivi sempre anche GetHashCode.
  • 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 con ToList() 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.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA