CodeGym /Cours /C# SELF /Ensemble : HashSet<T>...

Ensemble : HashSet<T>

C# SELF
Niveau 27 , Leçon 4
Disponible

1. Introduction

Imagine : tu codes un système pour un e-shop, et tu dois garder la liste de tous les codes produits uniques qui ont déjà été vendus. Ou mieux, tu écris une appli de réseau social, et tu veux checker vite fait si un nom d'utilisateur existe déjà. Et si tu veux compter combien de mots uniques il y a dans un gros texte ?

Dans tous ces scénarios, il te faut un ensemble d'éléments où chaque élément n'apparaît qu'une seule fois. Et c'est là que HashSet<T> entre en scène.

HashSet<T> — c'est une collection qui garde un ensemble non ordonné d'éléments uniques. Le mot clé ici c'est unique. Si tu essaies d'ajouter un élément qui est déjà dans le HashSet, il va juste ignorer ta tentative et ne va pas ajouter le doublon. C'est comme un club "réservé aux gens uniques" : si t'es déjà dedans, on te laisse pas rentrer une deuxième fois.

Points clés de HashSet<T> :

  • Unicité : Garantit que chaque élément de la collection est présent une seule fois.
  • Performance : Vérifie super vite la présence d'un élément, l'ajoute ou le supprime. En moyenne, ces opérations se font en temps constant (O(1)), peu importe le nombre d'éléments ! C'est possible grâce à un truc qui s'appelle hachage.
  • Pas d'ordre : Contrairement à List<T>, les éléments dans HashSet<T> ne sont pas stockés dans un ordre précis. Tu peux pas choper un élément par son index (genre "le cinquième élément").
  • Basé sur une table de hachage : À l'intérieur, HashSet<T> utilise une table de hachage pour stocker les éléments, ce qui explique sa rapidité. On va pas rentrer dans les détails du fonctionnement des tables de hachage (c'est pour une conf plus avancée), mais imagine que chaque élément "devient" un code numérique spécial (hash), qui permet de le retrouver super vite.

Comparons avec le fait d'utiliser une List<T> pour stocker des éléments uniques : tu devrais parcourir toute la liste à chaque fois pour vérifier que l'élément n'y est pas avant de l'ajouter. Ce serait super lent avec de grandes listes. HashSet<T> fait ça instantanément !

2. Pourquoi un(e) dev aurait besoin de HashSet<T> ?

Le secret des collections uniques

En dev, on a souvent ce souci : il faut stocker des éléments sans doublons. Par exemple, tu parses une liste d'adresses email d'utilisateurs de ton appli et tu veux être sûr qu'il n'y a pas de doublons. Ou tu récupères les noms de fichiers uniques d'un dossier. La solution la plus simple — une collection où tu peux pas ajouter deux fois la même chose.

Bien sûr, tu pourrais essayer de régler ça avec une List<T>, en checkant à la main si l'élément est déjà dans la liste avant de l'ajouter :

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

Mais cette solution rame grave sur de gros volumes — le check Contains sur une List doit regarder tous les éléments, et si t'as des milliers d'utilisateurs, ton programme va ramer comme un vieux PC sous Windows XP.

Ce que fait HashSet<T>

HashSet<T>, lui, garantit que chaque élément sera stocké une seule fois. Il est basé sur une table de hachage (comme un dictionnaire), donc les opérations d'ajout, de recherche et de suppression sont super rapides — en général en temps constant, sans devoir tout parcourir.

3. Les bases de HashSet<T>

Déclaration et création

Pour commencer, pas besoin d'ajouter de bibliothèques — la classe est déjà dans l'espace de noms System.Collections.Generic.

using System.Collections.Generic;

var emails = new HashSet<string>();

Tu peux remplir direct la collection avec des valeurs initiales, en les passant au constructeur :

var fruits = new HashSet<string> { "pomme", "banane", "poire", "banane" };
// "banane" apparaît deux fois, mais sera stocké qu'une seule fois !

Ajouter des éléments

Pour ajouter des éléments, utilise la méthode Add. Si l'élément n'était pas là, la méthode renvoie true. Si l'élément existe déjà — il se passe rien, et la méthode renvoie false.

bool added = emails.Add("vasya@example.com"); // true, élément ajouté
added = emails.Add("vasya@example.com");      // false, déjà là, pas ajouté

Fun fact : Tu peux appeler Add cent fois avec la même valeur — HashSet s'en fiche, il ignore juste les doublons.

Vérifier la présence : Contains

Pour checker si un élément est là, utilise la méthode Contains :

if (emails.Contains("vasya@example.com"))
    Console.WriteLine("Cet email existe déjà !");

Supprimer des éléments

La suppression — c'est rapide aussi :

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

Si l'élément n'était pas là — pas grave, la méthode renvoie juste false.

4. Exemple pratique

On va un peu corser notre CRM étudiante qu'on développe tout au long du cours.

Exigence

Imaginons que, dans notre système, chaque utilisateur doit avoir un nom d'utilisateur (login) unique. Avant d'ajouter un nouvel utilisateur, il faut vérifier l'unicité, et si jamais — prévenir.

Exemple de code

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Collection pour stocker les logins uniques
        var userNames = new HashSet<string>();

        while (true)
        {
            Console.Write("Entre un nom d'utilisateur (ou leave pour quitter) : ");
            string name = Console.ReadLine();

            if (name == "leave")
                break;

            if (userNames.Add(name))
            {
                Console.WriteLine("Nom ajouté avec succès !");
            }
            else
            {
                Console.WriteLine("Erreur : ce nom est déjà pris, essaie un autre.");
            }
        }

        Console.WriteLine("Liste des utilisateurs :");
        foreach (var user in userNames)
            Console.WriteLine($"- {user}");

        // Attention ! L'ordre d'affichage peut être aléatoire.
    }
}

Voilà, on a assuré l'unicité. Pas besoin de checker à la main — HashSet gère tout.

5. Comment HashSet<T> fonctionne à l'intérieur ? Pourquoi un hash code ?

Analogie : des cases de rangement

Imagine que t'as une grosse pile de cartes avec des logins, et une table avec des cases de 0 à 1000. Chaque login tu le mets dans une case, dont le numéro est calculé par une fonction (GetHashCode). Si les cartes sont identiques — elles tombent dans la même case, et tu sais vite que le login existe déjà.

La fonction GetHashCode

HashSet<T> compare les éléments pas juste par valeur, mais d'abord il calcule leur hash code avec la méthode GetHashCode(). Pour la plupart des types intégrés (int, string, double etc.), c'est déjà optimisé.

Fun fact : Si tu crées tes propres classes et que tu veux les stocker dans un HashSet<T>, pense à bien implémenter les méthodes pour comparer l'égalité et obtenir un code unique (méthodes Equals et GetHashCode), pour que l'unicité marche bien. Mais ça, on verra dans les prochaines confs.

Quelques erreurs classiques avec HashSet<T>

Quand on commence à utiliser une collection de valeurs uniques, on tombe souvent dans ce piège : croire que HashSet<T> garde les éléments dans l'ordre où on les a ajoutés. C'est faux ! Les hash sets ne garantissent aucun ordre, tout peut être dans un ordre random. Si l'ordre est important — il faut une autre collection, genre SortedSet<T>, mais c'est une autre histoire.

Deuxième erreur fréquente — essayer d'utiliser un index :

string name = userNames[0]; // Erreur ! HashSet<T> n'a pas d'index.

Contrairement à un tableau ou une liste, ici tu peux pas accéder à un élément par son numéro. Tu peux juste parcourir les éléments avec foreach.

Troisième confusion courante : quand tu veux sérialiser ou sauvegarder un hash set dans un fichier — à cause de l'ordre indéfini, les éléments peuvent être dans un ordre différent à chaque lancement du programme.

6. Opérations sur les ensembles : union, intersection, différence

HashSet<T> propose tout un tas de méthodes qui rendent son utilisation proche des ensembles mathématiques. Genre : union, intersection, différence et différence symétrique.

Voici les principales :

Méthode Ce qu'elle fait
UnionWith(other)
Ajoute dans le hash set tous les éléments de other.
IntersectWith(other)
Garde seulement les éléments présents ici ET dans other.
ExceptWith(other)
Supprime du set courant les éléments de other.
SymmetricExceptWith(other)
Garde seulement les éléments qui sont soit ici, soit dans other, mais pas dans les deux.

Exemple : intersection et union

Un exemple. Imaginons deux groupes de prénoms :

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

// Trouvons qui est dans les deux groupes
var common = new HashSet<string>(groupA); // on copie le contenu, sinon groupA va changer !
common.IntersectWith(groupB);

Console.WriteLine("Dans les deux groupes :");
foreach (var name in common)
    Console.WriteLine(name); // Affichera "Vera"

// Union de tous les étudiants des deux groupes, pour que personne ne soit oublié :
var all = new HashSet<string>(groupA);
all.UnionWith(groupB);

Console.WriteLine("Tous les étudiants :");
foreach (var name in all)
    Console.WriteLine(name); // "Anya", "Boris", "Vera", "Gleb", "Dasha"

7. Méthodes et propriétés supplémentaires

Count — pour savoir combien d'éléments il y a dans l'ensemble :

Console.WriteLine(userNames.Count);

Clear — tout supprimer (genre CTRL+A, DELETE dans la vraie vie) :

userNames.Clear();

SetEquals, IsSubsetOf, IsSupersetOf — pour vérifier si les ensembles sont égaux, si l'un est inclus dans l'autre, etc. Pratique si tu joues (ou codes) un truc genre "mathématicien — qui est le boss".

if (groupA.IsSubsetOf(groupB))
    Console.WriteLine("Tous ceux du groupe A sont dans le groupe B");

8. Stocker ses propres objets dans HashSet<T>

Comme on l'a déjà dit plus haut, les types standards savent déjà bien calculer leur hash et se comparer pour l'égalité.

Mais si tu veux stocker, par exemple, des utilisateurs comme objets, il faudra comparer selon un critère (genre le 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();
    }
}

// Maintenant tu peux :
var users = new HashSet<User>();
users.Add(new User { Login = "vasya" });
users.Add(new User { Login = "petya" });
users.Add(new User { Login = "vasya" }); // Ne sera pas ajouté !

Sans override des méthodes Equals et GetHashCode, HashSet va considérer tous les objets comme différents (même si le login est le même), car par défaut il compare les adresses en mémoire.

2
Mission
C# SELF, niveau 27, leçon 4
Bloqué
Vérification de l'unicité des chaînes
Vérification de l'unicité des chaînes
1
Étude/Quiz
Aperçu des collections principales, niveau 27, leçon 4
Indisponible
Aperçu des collections principales
Types de collections et generics
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION