Un HashSet<T> est une collection dans laquelle chaque élément apparaît au plus une fois. Il n'a ni index ni ordre garanti, et en échange il teste l'appartenance en un temps à peu près constant : trouver une étiquette parmi un million prend à peu près autant de temps que parmi dix.
Ajouter des éléments : Add renvoie un bool
Sortie :
True
False
3
True
True
False
2
Ajouter un doublon n'est pas une erreur ; Add renvoie simplement false et l'ensemble reste inchangé. Cette valeur de retour est ce que la méthode a de plus utile. Elle combine « l'ai-je déjà vu ? » et « retiens-le » en un seul appel :
Sortie :
Duplicate: ana@x.com
Duplicate: ben@x.com
3 unique
Pourquoi Contains est rapide
List<T>.Contains compare la valeur à chaque élément tour à tour, donc son coût augmente avec la liste. Un HashSet<T> calcule le code de hachage de l'élément, saute au compartiment correspondant et ne compare que les quelques éléments qui y sont stockés. Pour un test d'appartenance dans une boucle, cela transforme une étape en O(n) en une étape en O(1), et une boucle imbriquée sur deux listes en un seul passage :
// 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));
Construire l'ensemble coûte un passage sur la liste, ce n'est donc rentable que si vous faites plus que quelques recherches.
Supprimer les doublons d'une liste
Il existe trois façons courantes, qui diffèrent par ce qu'elles font de l'ordre :
Sortie :
Lima, Oslo, Pune, Kyiv
4
Lima, Oslo, Pune, Kyiv
Distinct est le bon choix par défaut quand vous voulez récupérer une liste. La version sur place fonctionne parce que RemoveAll appelle le prédicat une fois par élément, dans l'ordre : seen.Add renvoie false pour la deuxième copie et les suivantes, et ce sont exactement celles-là qui sont supprimées.
Opérations ensemblistes : union, intersection, différence
HashSet<T> dispose des opérations de la théorie des ensembles. Les méthodes ...With modifient l'ensemble sur lequel elles sont appelées et ne renvoient rien.
Sortie :
Union: Ana, Ben, Chloe, Dev
Intersect: Ben, Chloe
Except: Ana
Symmetric: Ana, Dev
True
True
False
True
L'argument peut être n'importe quel IEnumerable<T> : un tableau, une liste ou un autre ensemble. SetEquals ignore l'ordre et les doublons de l'argument. La fonction Show trie avant d'afficher, car l'ordre d'énumération d'un ensemble n'est pas fiable.
LINQ a des méthodes correspondantes qui renvoient une nouvelle séquence et laissent les entrées intactes : monday.Union(tuesday), monday.Intersect(tuesday), monday.Except(tuesday). Utilisez-les quand vous ne voulez pas modifier un ensemble, ou quand les entrées sont des listes.
Égalité personnalisée pour vos propres classes
Un ensemble décide qu'il s'agit du « même élément » avec GetHashCode et Equals. Pour une classe qui ne les redéfinit pas, les deux reposent sur l'identité de l'objet, donc deux objets aux champs égaux sont deux éléments différents :
Sortie :
2
1
True
La règle : des objets égaux selon Equals doivent renvoyer le même GetHashCode. Redéfinissez seulement Equals et l'ensemble cherche dans le mauvais compartiment et signale toujours des doublons. Sur .NET Core 2.1 et plus, HashCode.Combine(X, Y) construit un bon code de hachage sans arithmétique écrite à la main.
Quand vous ne pouvez pas modifier la classe, ou qu'il vous faut une autre notion de « même » pour un ensemble donné, passez un IEqualityComparer<T> au constructeur. Les chaînes ont des comparateurs tout prêts :
Sortie :
True
False
2
Depuis C# 9, un record génère pour vous Equals et GetHashCode basés sur les valeurs, donc record Point(int X, int Y); fonctionne dans un ensemble sans code supplémentaire.
Ne modifiez jamais un champ qui entre dans GetHashCode pendant que l'objet est dans un ensemble. L'objet reste dans le compartiment de son ancien hachage, donc Contains et Remove ne le trouvent plus.
Ordre et SortedSet
Un HashSet<T> s'énumère dans un ordre que vous devez considérer comme arbitraire. Si vous avez besoin d'éléments triés, triez à l'affichage (set.OrderBy(x => x)) ou utilisez SortedSet<T>, qui garde les éléments ordonnés en permanence et ajoute Min, Max et des requêtes par plage, en O(log n) par opération :
var ranks = new SortedSet<int> { 30, 10, 20 };
Console.WriteLine(string.Join(", ", ranks)); // 10, 20, 30
Console.WriteLine(ranks.Min); // 10
HashSet, List ou Dictionary
| Besoin | Utiliser |
|---|---|
| Éléments uniques, « est-il présent ? » rapide | HashSet<T> |
| Éléments uniques, toujours triés | SortedSet<T> |
| Ordre, doublons, accès par index | List<T> |
| Une valeur stockée sous chaque clé | Dictionary<TKey, TValue> |
Un ensemble est un dictionnaire avec des clés et sans valeurs. Si vous vous surprenez à écrire Dictionary<string, bool> juste pour suivre l'appartenance, un HashSet<string> dit la même chose plus clairement.
Erreurs courantes
- S'attendre à un ordre. Un ensemble n'en a aucun sur lequel compter ; triez, ou utilisez
SortedSet<T>. - Des classes personnalisées sans
EqualsniGetHashCode. Des objets qui semblent égaux deviennent des éléments distincts. - Redéfinir
Equalsseul. Redéfinissez toujoursGetHashCodeavec. - Modifier un élément après l'avoir ajouté. L'ensemble ne peut plus le trouver.
- Indexer un ensemble.
set[0]ne compile pas ; il n'y a pas d'index. Convertissez avecToList()si vous avez besoin de positions.
Questions fréquentes
Qu'est-ce qu'un HashSet en C# ?
HashSet<T> est une collection d'éléments uniques sans ordre défini. Ajouter un élément déjà présent ne fait rien, et Contains répond en un temps à peu près constant quelle que soit la taille de l'ensemble, car les éléments sont stockés par code de hachage comme les clés d'un Dictionary.
Que renvoie HashSet.Add ?
Add renvoie true quand l'élément a été ajouté et false quand il était déjà dans l'ensemble. Cela fait de if (!seen.Add(x)) un test de doublon en une ligne : il ajoute les nouveaux éléments et vous signale les répétitions dans le même appel.
Comment supprimer les doublons d'une List en C# ?
list.Distinct().ToList() renvoie une nouvelle liste sans doublons et garde la première occurrence de chaque élément dans son ordre d'origine. new HashSet<T>(list) supprime aussi les doublons, mais un ensemble n'a pas d'ordre garanti. Pour dédoublonner sur place, utilisez var seen = new HashSet<T>(); list.RemoveAll(x => !seen.Add(x));.
Quand utiliser un HashSet plutôt qu'une List ?
Utilisez un HashSet<T> quand vous demandez surtout « cet élément est-il dans la collection ? » ou que les éléments doivent être uniques. List<T>.Contains parcourt chaque élément, donc ralentit à mesure que la liste grandit, alors que HashSet<T>.Contains non. Utilisez une List<T> quand l'ordre, les doublons ou l'accès par index comptent.
Pourquoi mon HashSet contient-il des objets en double ?
Votre classe ne redéfinit pas Equals et GetHashCode, donc l'ensemble compare des références et deux objets aux mêmes valeurs de champs comptent comme différents. Redéfinissez les deux méthodes (toujours ensemble), ou passez un IEqualityComparer<T> au constructeur de l'ensemble.