Menu

C# HashSet: eindeutige Elemente, Contains und Mengenoperationen

HashSet<T> enthält eindeutige Elemente und beantwortet Contains in konstanter Zeit. Lerne, wie Add Duplikate meldet, wie du Duplikate aus einer Liste entfernst, wie du Mengen vereinigst, schneidest und subtrahierst und wie ein Set deine eigenen Objekte nach Wert vergleicht.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Ein HashSet<T> ist eine Collection, in der jedes Element höchstens einmal vorkommt. Es hat keinen Index und keine garantierte Reihenfolge, und im Gegenzug prüft es die Zugehörigkeit in ungefähr konstanter Zeit: Ein Tag unter einer Million zu finden dauert etwa so lange wie unter zehn.

Elemente hinzufügen: Add gibt bool zurück

Ausgabe:

True
False
3
True
True
False
2

Ein Duplikat hinzuzufügen ist kein Fehler; Add gibt einfach false zurück, und das Set bleibt unverändert. Dieser Rückgabewert ist das Nützlichste an der Methode. Er verbindet „habe ich das schon gesehen?“ und „merk es dir“ in einem Aufruf:

Ausgabe:

Duplicate: ana@x.com
Duplicate: ben@x.com
3 unique

Warum Contains schnell ist

List<T>.Contains vergleicht den Wert der Reihe nach mit jedem Element, die Kosten wachsen also mit der Liste. Ein HashSet<T> berechnet den Hashcode des Elements, springt zum Bucket für diesen Code und vergleicht nur die wenigen dort gespeicherten Elemente. Für einen Zugehörigkeitstest in einer Schleife wird so aus einem O(n)-Schritt ein O(1)-Schritt und aus einer verschachtelten Schleife über zwei Listen ein einziger Durchgang:

// 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));

Das Set aufzubauen kostet einen Durchgang über die Liste, es lohnt sich also nur, wenn du mehr als eine Handvoll Mal nachschlägst.

Duplikate aus einer Liste entfernen

Es gibt drei übliche Wege, und sie unterscheiden sich darin, was mit der Reihenfolge passiert:

Ausgabe:

Lima, Oslo, Pune, Kyiv
4
Lima, Oslo, Pune, Kyiv

Distinct ist der richtige Standard, wenn du eine Liste zurückbekommen willst. Die Version an Ort und Stelle funktioniert, weil RemoveAll das Prädikat der Reihe nach einmal pro Element aufruft: seen.Add gibt für die zweite und jede weitere Kopie false zurück, genau diese werden also entfernt.

Mengenoperationen: Vereinigung, Schnittmenge, Differenz

HashSet<T> hat die Operationen aus der Mengenlehre. Die Methoden auf ...With ändern das Set, auf dem sie aufgerufen werden, und geben nichts zurück.

Ausgabe:

Union:     Ana, Ben, Chloe, Dev
Intersect: Ben, Chloe
Except:    Ana
Symmetric: Ana, Dev
True
True
False
True

Das Argument kann jedes IEnumerable<T> sein: ein Array, eine Liste oder ein anderes Set. SetEquals ignoriert Reihenfolge und Duplikate im Argument. Die Hilfsmethode Show sortiert vor der Ausgabe, weil man sich auf die Reihenfolge beim Durchlaufen eines Sets nicht verlassen sollte.

LINQ hat passende Methoden, die eine neue Sequenz zurückgeben und die Eingaben unangetastet lassen: monday.Union(tuesday), monday.Intersect(tuesday), monday.Except(tuesday). Nimm diese, wenn du ein Set nicht verändern willst oder die Eingaben Listen sind.

Eigene Gleichheit für deine Klassen

Ein Set entscheidet mit GetHashCode und Equals, was „dasselbe Element“ ist. Bei einer Klasse, die sie nicht überschreibt, beruhen beide auf der Identität des Objekts, zwei Objekte mit gleichen Feldern sind also zwei verschiedene Elemente:

Ausgabe:

2
1
True

Die Regel: Objekte, die per Equals gleich sind, müssen denselben GetHashCode zurückgeben. Überschreibst du nur Equals, sucht das Set im falschen Bucket und meldet trotzdem Duplikate. Ab .NET Core 2.1 baut HashCode.Combine(X, Y) einen guten Hashcode ohne handgeschriebene Arithmetik.

Wenn du die Klasse nicht ändern kannst oder für ein Set einen anderen Begriff von „gleich“ brauchst, übergib dem Konstruktor einen IEqualityComparer<T>. Für Strings gibt es fertige Comparer:

Ausgabe:

True
False
2

Ab C# 9 erzeugt ein record wertbasierte Equals und GetHashCode für dich, record Point(int X, int Y); funktioniert in einem Set also ohne zusätzlichen Code.

Ändere nie ein Feld, das in GetHashCode einfließt, solange das Objekt in einem Set ist. Das Objekt bleibt im Bucket seines alten Hashs, und Contains und Remove finden es nicht mehr.

Reihenfolge und SortedSet

Ein HashSet<T> durchläuft in einer Reihenfolge, die du als beliebig behandeln solltest. Wenn du die Elemente sortiert brauchst, sortiere bei der Ausgabe (set.OrderBy(x => x)) oder nimm SortedSet<T>, das die Elemente jederzeit geordnet hält und Min, Max und Bereichsabfragen mit O(log n) pro Operation bietet:

var ranks = new SortedSet<int> { 30, 10, 20 };
Console.WriteLine(string.Join(", ", ranks)); // 10, 20, 30
Console.WriteLine(ranks.Min);                // 10

HashSet gegenüber List gegenüber Dictionary

BedarfVerwende
Eindeutige Elemente, schnelles „ist es da?“HashSet<T>
Eindeutige Elemente, immer sortiertSortedSet<T>
Reihenfolge, Duplikate, IndexzugriffList<T>
Ein Wert unter jedem SchlüsselDictionary<TKey, TValue>

Ein Set ist ein Dictionary mit Schlüsseln und ohne Werte. Wenn du Dictionary<string, bool> schreibst, nur um die Zugehörigkeit festzuhalten, sagt ein HashSet<string> dasselbe klarer.

Häufige Fehler

  • Eine Reihenfolge erwarten. Ein Set hat keine, auf die du dich verlassen kannst; sortiere oder nimm SortedSet<T>.
  • Eigene Klassen ohne Equals und GetHashCode. Gleich aussehende Objekte werden zu separaten Elementen.
  • Nur Equals überschreiben. Überschreibe immer GetHashCode mit.
  • Ein Element nach dem Hinzufügen verändern. Das Set findet es nicht mehr.
  • Ein Set indizieren. set[0] kompiliert nicht; es gibt keinen Index. Wandle mit ToList() um, wenn du Positionen brauchst.

Häufig gestellte Fragen

Was ist ein HashSet in C#?

HashSet<T> ist eine Collection eindeutiger Elemente ohne festgelegte Reihenfolge. Ein Element hinzuzufügen, das schon vorhanden ist, bewirkt nichts, und Contains antwortet in ungefähr konstanter Zeit, egal wie groß das Set ist, weil Elemente wie die Schlüssel eines Dictionary nach Hashcode gespeichert werden.

Was gibt HashSet.Add zurück?

Add gibt true zurück, wenn das Element hinzugefügt wurde, und false, wenn es schon im Set war. Damit ist if (!seen.Add(x)) eine einzeilige Duplikatprüfung: Sie fügt neue Elemente hinzu und meldet wiederholte im selben Aufruf.

Wie entferne ich in C# Duplikate aus einer List?

list.Distinct().ToList() gibt eine neue Liste ohne Duplikate zurück und behält das erste Vorkommen jedes Elements in seiner ursprünglichen Reihenfolge. new HashSet<T>(list) entfernt ebenfalls Duplikate, aber ein Set hat keine garantierte Reihenfolge. Um an Ort und Stelle zu deduplizieren, nimm var seen = new HashSet<T>(); list.RemoveAll(x => !seen.Add(x));.

Wann sollte ich ein HashSet statt einer List verwenden?

Nimm ein HashSet<T>, wenn du hauptsächlich fragst „ist dieses Element in der Collection?“ oder die Elemente eindeutig sein müssen. List<T>.Contains durchsucht jedes Element und wird mit wachsender Liste langsamer, HashSet<T>.Contains nicht. Nimm eine List<T>, wenn Reihenfolge, Duplikate oder Indexzugriff zählen.

Warum enthält mein HashSet doppelte Objekte?

Deine Klasse überschreibt Equals und GetHashCode nicht, also vergleicht das Set Referenzen, und zwei Objekte mit denselben Feldwerten gelten als verschieden. Überschreibe beide Methoden (immer zusammen) oder übergib dem Konstruktor des Sets einen IEqualityComparer<T>.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S