CodeGym /Corsi /C# SELF /Insieme: HashSet<T><...

Insieme: HashSet<T>

C# SELF
Livello 27 , Lezione 4
Disponibile

1. Introduzione

Immagina questa situazione: stai sviluppando un sistema per un e-commerce e devi salvare la lista di tutti i codici prodotto unici che sono mai stati venduti. Oppure, ancora meglio, stai scrivendo un'app per un social network e devi controllare al volo se un certo username esiste già. E se vuoi contare quante parole uniche ci sono in un testo gigante?

In tutti questi casi ci serve un insieme di elementi dove ogni elemento compare solo una volta. Ed è qui che entra in gioco HashSet<T>.

HashSet<T> è una collezione che salva un insieme non ordinato di elementi unici. La parola chiave qui è unici. Se provi ad aggiungere un elemento che già c'è nel HashSet, semplicemente ignora il tentativo e non aggiunge il duplicato. È come un club "solo per persone uniche": se sei già dentro, la seconda volta non ti fanno entrare.

Caratteristiche chiave di HashSet<T>:

  • Unicità: Garantisce che ogni elemento nella collezione sia presente solo una volta.
  • Performance: Controlla la presenza, aggiunge e rimuove elementi super velocemente. In media queste operazioni sono tempo costante (O(1)), non importa quanti elementi ci sono! Questo grazie a un meccanismo chiamato hashing.
  • Nessun ordine: Diversamente da List<T>, gli elementi in HashSet<T> non sono in nessun ordine particolare. Non puoi prendere un elemento per indice (tipo "il quinto elemento").
  • Basato su hash table: Dentro, HashSet<T> usa una hash table per salvare gli elementi, ed è questo che lo rende così veloce. Non andiamo ora nei dettagli di come funzionano le hash table (è roba da lezione avanzata), ma pensa che ogni elemento "diventa" un codice numerico speciale (hash), e così lo trovano subito.

Facciamo un confronto: se usassi List<T> per salvare solo elementi unici, dovresti ogni volta scorrere tutta la lista per controllare che l'elemento non ci sia già prima di aggiungerlo. Sarebbe lentissimo con liste grandi. HashSet<T> lo fa in un attimo!

2. Perché serve HashSet<T> a un programmatore?

Il mistero delle collezioni uniche

Nel coding capita spesso: devi salvare elementi senza ripetizioni. Tipo, stai parsando una lista di email degli utenti dell'app e vuoi essere sicuro che non ci siano doppioni. Oppure raccogli i nomi unici dei file letti da una cartella. La soluzione più semplice — una collezione dove non puoi aggiungere due volte la stessa cosa.

Certo, potresti provare a risolvere con una List<T>, controllando a mano se l'elemento c'è già prima di aggiungerlo:

var users = new List<string>();
if (!users.Contains("vasya@example.com"))
    users.Add("vasya@example.com");

Ma questa soluzione va male con tanti dati — il controllo Contains su una List deve guardare tutti gli elementi, e se hai migliaia di utenti, il programma diventa lento come un vecchio PC con Windows XP.

Cosa fa HashSet<T>

HashSet<T>, invece, garantisce che ogni elemento sia salvato solo una volta. È fatto sopra una hash table (come un dizionario), quindi aggiunta, ricerca e rimozione sono super veloci — di solito tempo costante, senza dover guardare tutto.

3. Basi di utilizzo di HashSet<T>

Dichiarazione e creazione

Per iniziare, non serve importare nessuna libreria extra — la classe è già nello spazio dei nomi System.Collections.Generic.

using System.Collections.Generic;

var emails = new HashSet<string>();

Puoi anche riempire subito la collezione con valori iniziali, passandoli al costruttore:

var fruits = new HashSet<string> { "mela", "banana", "pera", "banana" };
// "banana" compare due volte, ma sarà salvata solo una volta!

Aggiunta di elementi

Aggiungi elementi col metodo Add. Se l'elemento non c'era, il metodo ritorna true. Se già c'è — non succede nulla e ritorna false.

bool added = emails.Add("vasya@example.com"); // true, elemento aggiunto
added = emails.Add("vasya@example.com");      // false, già c'è, non aggiunto

Divertente: Puoi chiamare Add anche cento volte con lo stesso valore — HashSet non si offende, semplicemente ignora i doppioni.

Controllo presenza: Contains

Per controllare se un elemento c'è, usa il metodo Contains:

if (emails.Contains("vasya@example.com"))
    Console.WriteLine("Questa email esiste già!");

Rimozione di elementi

Anche la rimozione è veloce:

emails.Remove("vasya@example.com");

Se l'elemento non c'era — nessun problema, il metodo ritorna solo false.

4. Esempio pratico

Rendiamo un po' più interessante il nostro CRM per studenti che stiamo sviluppando durante il corso.

Requisito

Supponiamo che, secondo le regole del nostro sistema, ogni utente debba avere un username unico (login). Prima di aggiungere un nuovo utente bisogna controllare l'unicità, e se serve — avvisare.

Esempio di codice

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Collezione per salvare login unici
        var userNames = new HashSet<string>();

        while (true)
        {
            Console.Write("Inserisci username (o leave per uscire): ");
            string name = Console.ReadLine();

            if (name == "leave")
                break;

            if (userNames.Add(name))
            {
                Console.WriteLine("Username aggiunto con successo!");
            }
            else
            {
                Console.WriteLine("Errore: questo username è già preso, prova un altro.");
            }
        }

        Console.WriteLine("Lista utenti:");
        foreach (var user in userNames)
            Console.WriteLine($"- {user}");

        // Attenzione! L'ordine di stampa può essere casuale.
    }
}

Così abbiamo garantito l'unicità in modo semplice. Non serve controllare a mano — HashSet fa tutto lui.

5. Come funziona HashSet<T> dentro? A cosa serve l'hash code

Analogia: celle per conservare

Immagina di avere una pila gigante di cartoncini con i login, e un tavolo con celle numerate da 0 a 1000. Ogni login lo metti nella cella il cui numero è calcolato da una funzione (GetHashCode). Se i cartoncini coincidono — finiscono nella stessa cella, e scopri subito che il login esiste già.

Funzione GetHashCode

HashSet<T> confronta gli elementi non solo per valore, ma prima calcola il loro hash code col metodo GetHashCode(). Per la maggior parte dei tipi built-in (int, string, double ecc.) è già fatto bene.

Curiosità: Se crei le tue classi e vuoi salvarle in un HashSet<T>, devi implementare i metodi giusti per il confronto e per ottenere il codice unico (Equals e GetHashCode), così l'unicità funziona bene. Ma di questo parleremo nelle prossime lezioni.

Alcuni errori tipici con HashSet<T>

Quando si inizia a usare una collezione di valori unici, capita spesso questa trappola: pensare che HashSet<T> salvi gli elementi nell'ordine in cui li hai aggiunti. Non è così! Gli hash set non garantiscono nessun ordine, tutto può essere in ordine casuale. Se ti serve l'ordine — serve un altro tipo di collezione, tipo SortedSet<T>, ma questa è un'altra storia.

Secondo errore comune — provare a usare l'indice:

string name = userNames[0]; // Errore! HashSet<T> non ha indici.

Diversamente da array o lista, qui non puoi accedere per numero. Puoi solo scorrere gli elementi con foreach.

Terzo errore diffuso: quando serializzi o salvi l'hash set su file — visto che l'ordine non è garantito, tra un avvio e l'altro gli elementi possono essere in sequenza diversa.

6. Operazioni sugli insiemi: unione, intersezione, differenza

HashSet<T> offre un sacco di metodi che lo fanno sembrare proprio come un insieme matematico. Tipo: unione, intersezione, differenza e differenza simmetrica.

Ecco i principali:

Metodo Cosa fa
UnionWith(other)
Aggiunge all'hash set tutti gli elementi di other.
IntersectWith(other)
Lascia solo gli elementi che ci sono sia qui che in other.
ExceptWith(other)
Rimuove dal set corrente gli elementi di other.
SymmetricExceptWith(other)
Lascia solo gli elementi che ci sono o qui o in other, ma non in entrambi.

Esempio: intersezione e unione

Facciamo un esempio. Supponiamo di avere due gruppi di nomi:

var groupA = new HashSet<string> { "Anja", "Boris", "Vera" };
var groupB = new HashSet<string> { "Vera", "Gleb", "Dasha" };

// Troviamo chi c'è in entrambi i gruppi
var common = new HashSet<string>(groupA); // copiamo il contenuto, altrimenti groupA cambia!
common.IntersectWith(groupB);

Console.WriteLine("In entrambi i gruppi:");
foreach (var name in common)
    Console.WriteLine(name); // Stampa "Vera"

// Uniamo tutti gli studenti di entrambi i gruppi, così nessuno si perde:
var all = new HashSet<string>(groupA);
all.UnionWith(groupB);

Console.WriteLine("Tutti gli studenti:");
foreach (var name in all)
    Console.WriteLine(name); // "Anja", "Boris", "Vera", "Gleb", "Dasha"

7. Altri metodi e proprietà utili

Count — per sapere quanti elementi ci sono nell'insieme:

Console.WriteLine(userNames.Count);

Clear — cancella tutto (tipo CTRL+A, DELETE nella vita reale):

userNames.Clear();

SetEquals, IsSubsetOf, IsSupersetOf — controlli per vedere se gli insiemi sono uguali, uno è dentro l'altro ecc. Utile se stai programmando (o giocando) qualcosa tipo "matematico — chi è più forte".

if (groupA.IsSubsetOf(groupB))
    Console.WriteLine("Tutti quelli del gruppo A sono anche in gruppo B");

8. Salvare oggetti custom in HashSet<T>

Come già accennato sopra, i tipi standard sanno già calcolare bene gli hash e confrontarsi per uguaglianza.

Ma se vuoi salvare, ad esempio, utenti come oggetti, devi assicurarti che il confronto sia fatto su qualcosa di preciso (tipo il login):

class User
{
    public string Login { get; set; }

    public override bool Equals(object obj)
    {
        if (obj is User other)
            return Login == other.Login;
        return false;
    }

    public override int GetHashCode()
    {
        return Login.GetHashCode();
    }
}

// Ora puoi:
var users = new HashSet<User>();
users.Add(new User { Login = "vasya" });
users.Add(new User { Login = "petya" });
users.Add(new User { Login = "vasya" }); // Non verrà aggiunto!

Senza gli override dei metodi Equals e GetHashCode, HashSet considererà tutte le istanze diverse (anche se il login è uguale), perché di default confronta gli indirizzi in memoria.

1
Sondaggio/quiz
Panoramica delle collezioni principali, livello 27, lezione 4
Non disponibile
Panoramica delle collezioni principali
Tipi di collezioni e generics
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION