CodeGym /Kurse /C# SELF /Menge: HashSet<T>

Menge: HashSet<T>

C# SELF
Level 27 , Lektion 4
Verfügbar

1. Einführung

Stell dir vor: Du entwickelst ein System für einen Online-Shop und musst eine Liste aller einzigartigen Produktcodes speichern, die jemals verkauft wurden. Oder noch besser: Du schreibst eine Social-Media-App und willst schnell checken, ob ein Benutzername schon existiert. Oder du willst wissen, wie viele einzigartige Wörter in einem großen Text vorkommen?

In all diesen Szenarien brauchst du eine Sammlung, in der jedes Element nur einmal vorkommt. Und hier kommt HashSet<T> ins Spiel.

HashSet<T> ist eine Collection, die eine ungeordnete Menge von einzigartigen Elementen speichert. Das wichtigste Wort hier ist einzigartig. Wenn du versuchst, ein Element hinzuzufügen, das schon im HashSet ist, ignoriert es einfach deinen Versuch und fügt kein Duplikat hinzu. Das ist wie ein Club "nur für einzigartige Leute": Wenn du schon drin bist, kommst du kein zweites Mal rein.

Wichtige Eigenschaften von HashSet<T>:

  • Einzigartigkeit: Garantiert, dass jedes Element in der Collection nur einmal vorkommt.
  • Performance: Prüft super schnell, ob ein Element drin ist, fügt es hinzu oder entfernt es. Im Durchschnitt laufen diese Operationen in konstanter Zeit (O(1)), egal wie viele Elemente drin sind! Das klappt dank einem Mechanismus namens Hashing.
  • Keine Reihenfolge: Im Gegensatz zu List<T> werden die Elemente im HashSet<T> nicht in einer bestimmten Reihenfolge gespeichert. Du kannst kein Element per Index bekommen (also z.B. "das fünfte Element").
  • Basierend auf Hash-Tabelle: Intern benutzt HashSet<T> eine Hash-Tabelle zum Speichern der Elemente, was die hohe Performance ermöglicht. Wir tauchen jetzt nicht tief in die Funktionsweise von Hash-Tabellen ein (das ist Stoff für eine eigene, fortgeschrittene Vorlesung), aber stell dir vor, jedes Element wird in einen speziellen Zahlencode (Hash) "verwandelt", mit dem man es dann super schnell findet.

Vergleichen wir das mal damit, wie es wäre, wenn du List<T> für einzigartige Elemente benutzt: Du müsstest jedes Mal die ganze Liste durchgehen, um sicher zu sein, dass das Element noch nicht drin ist, bevor du es hinzufügst. Das wäre bei großen Listen richtig langsam. HashSet<T> macht das sofort!

2. Wozu braucht ein Programmierer HashSet<T>?

Das Geheimnis der einzigartigen Collections

In der Programmierung gibt es oft die Aufgabe: Du musst Elemente ohne Wiederholungen speichern. Zum Beispiel, du parst eine Liste von E-Mail-Adressen von App-Usern und willst sicherstellen, dass es keine Duplikate gibt. Oder du sammelst einzigartige Dateinamen aus einem Ordner. Die einfachste Lösung: Eine Collection, in die du nicht zweimal das gleiche reinpacken kannst.

Klar, du könntest das mit List<T> lösen, indem du vor dem Hinzufügen manuell prüfst, ob das Element schon drin ist:

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

Aber das läuft bei großen Mengen schlecht – die Contains-Prüfung bei List muss alle Elemente durchgehen, und wenn du tausende User hast, wird das Programm so langsam wie ein alter Rechner mit Windows XP.

Was macht HashSet<T>

HashSet<T> dagegen garantiert, dass jedes Element nur einmal gespeichert wird. Es basiert auf einer Hash-Tabelle (wie ein Dictionary), das heißt, Hinzufügen, Suchen und Entfernen laufen richtig schnell – meistens in konstanter Zeit, ohne alles durchzuscrollen.

3. Grundlagen zur Arbeit mit HashSet<T>

Deklaration und Erzeugung

Um loszulegen, musst du keine extra Bibliotheken einbinden – die Klasse ist schon im Namespace System.Collections.Generic dabei.

using System.Collections.Generic;

var emails = new HashSet<string>();

Du kannst die Collection auch direkt mit Startwerten füllen, indem du sie dem Konstruktor übergibst:

var fruits = new HashSet<string> { "apfel", "banane", "birne", "banane" };
// "banane" kommt zweimal vor, wird aber nur einmal gespeichert!

Elemente hinzufügen

Elemente fügst du mit der Methode Add hinzu. Wenn das Element noch nicht drin war, gibt die Methode true zurück. Wenn es schon drin ist – passiert einfach nichts und die Methode gibt false zurück.

bool added = emails.Add("vasya@example.com"); // true, Element wurde hinzugefügt
added = emails.Add("vasya@example.com");      // false, schon drin, nicht hinzugefügt

Lustig: Du kannst Add hundertmal mit dem gleichen Wert aufrufen – HashSet ist nicht beleidigt, sondern ignoriert Wiederholungen einfach höflich.

Prüfen, ob ein Element drin ist: Contains

Um zu checken, ob ein Element gespeichert ist, benutze die Methode Contains:

if (emails.Contains("vasya@example.com"))
    Console.WriteLine("Diese E-Mail gibt es schon!");

Elemente entfernen

Entfernen geht auch schnell:

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

Wenn das Element nicht drin war – kein Problem, die Methode gibt einfach false zurück.

4. Praktisches Beispiel

Lass uns unsere Studenten-CRM, die wir im Kurs weiterentwickeln, ein bisschen aufbohren.

Anforderung

Angenommen, laut Systemregeln muss jeder User einen einzigartigen Benutzernamen (Login) haben. Vor dem Hinzufügen eines neuen Users musst du die Einzigartigkeit prüfen und ggf. eine Meldung ausgeben.

Beispielcode

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Collection zum Speichern einzigartiger Logins
        var userNames = new HashSet<string>();

        while (true)
        {
            Console.Write("Gib den Benutzernamen ein (oder leave zum Beenden): ");
            string name = Console.ReadLine();

            if (name == "leave")
                break;

            if (userNames.Add(name))
            {
                Console.WriteLine("Name erfolgreich hinzugefügt!");
            }
            else
            {
                Console.WriteLine("Fehler: Dieser Name ist schon vergeben, probier einen anderen.");
            }
        }

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

        // Achtung! Die Ausgabe-Reihenfolge kann beliebig sein.
    }
}

So einfach stellst du Einzigartigkeit sicher. Du musst nicht manuell prüfen – HashSet macht das für dich.

5. Wie funktioniert HashSet<T> innen? Wozu braucht man einen Hash-Code?

Analogie: Fächer zum Aufbewahren

Stell dir vor, du hast einen riesigen Stapel Karten mit Logins und einen Tisch mit Fächern von 0 bis 1000. Jeden Login legst du in das Fach, dessen Nummer eine Funktion (GetHashCode) berechnet. Wenn Karten gleich sind, landen sie im selben Fach und du merkst schnell, dass der Login schon existiert.

Die Funktion GetHashCode

HashSet<T> vergleicht Elemente nicht einfach nach Wert, sondern berechnet zuerst ihren Hash-Code mit der Methode GetHashCode(). Für die meisten eingebauten Typen (int, string, double usw.) ist das schon optimal implementiert.

Fun Fact: Wenn du eigene Klassen-Typen baust und sie im HashSet<T> speichern willst, solltest du unbedingt korrekte Methoden für Gleichheit und eindeutigen Code implementieren (Equals und GetHashCode), damit die Einzigartigkeit richtig funktioniert. Aber dazu mehr in den nächsten Vorlesungen.

Typische Fehler beim Arbeiten mit HashSet<T>

Wenn Leute gerade erst lernen, mit einzigartigen Collections zu arbeiten, tappen sie oft in diese Falle: Sie denken, dass HashSet<T> die Elemente in der Reihenfolge speichert, in der sie hinzugefügt wurden. Das stimmt nicht! HashSets garantieren keine Reihenfolge, alles kann zufällig angeordnet sein. Wenn dir die Reihenfolge wichtig ist, brauchst du einen anderen Collection-Typ, z.B. SortedSet<T>, aber das ist eine andere Geschichte.

Der zweite beliebte Fehler – Versuch, einen Index zu benutzen:

string name = userNames[0]; // Fehler! HashSet<T> hat keine Indizes.

Im Gegensatz zu Array oder Liste kannst du hier nicht per Nummer auf ein Element zugreifen. Durchlaufen geht nur mit foreach.

Der dritte verbreitete Stolperstein ist beim Serialisieren oder Speichern eines HashSets in eine Datei – wegen der unbestimmten Reihenfolge können die Elemente beim nächsten Programmstart in anderer Reihenfolge auftauchen.

6. Mengenoperationen: Vereinigung, Schnittmenge, Differenz

HashSet<T> bietet eine ganze Reihe von Methoden, mit denen du wie in der Mathematik mit Mengen arbeiten kannst. Zum Beispiel: Vereinigung, Schnittmenge, Differenz und symmetrische Differenz.

Hier die wichtigsten:

Methode Was sie macht
UnionWith(other)
Fügt dem HashSet alle Elemente aus other hinzu.
IntersectWith(other)
Lässt nur die Elemente übrig, die hier und in other sind.
ExceptWith(other)
Entfernt aus dem aktuellen Set die Elemente aus other.
SymmetricExceptWith(other)
Lässt nur die Elemente übrig, die entweder hier oder in other sind, aber nicht in beiden.

Beispiel: Schnittmenge und Vereinigung

Schauen wir uns das mal an. Angenommen, es gibt zwei Gruppen mit Namen:

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

// Wer ist in beiden Gruppen?
var common = new HashSet<string>(groupA); // Inhalt kopieren, sonst ändert sich groupA!
common.IntersectWith(groupB);

Console.WriteLine("In beiden Gruppen:");
foreach (var name in common)
    Console.WriteLine(name); // Gibt "Vera" aus

// Alle Studenten aus beiden Gruppen zusammenfassen, damit keiner verloren geht:
var all = new HashSet<string>(groupA);
all.UnionWith(groupB);

Console.WriteLine("Alle Studenten:");
foreach (var name in all)
    Console.WriteLine(name); // "Anja", "Boris", "Vera", "Gleb", "Daria"

7. Weitere Methoden und Eigenschaften

Count – zeigt dir, wie viele Elemente insgesamt im Set sind:

Console.WriteLine(userNames.Count);

Clear – alles löschen (wie STRG+A, ENTF im echten Leben):

userNames.Clear();

SetEquals, IsSubsetOf, IsSupersetOf – prüft, ob Mengen gleich sind, eine in der anderen steckt usw. Praktisch, wenn du (oder dein Code) "Mathematiker – wer ist cooler" spielst.

if (groupA.IsSubsetOf(groupB))
    Console.WriteLine("Alle aus Gruppe A sind in Gruppe B");

8. Eigene Objekte im HashSet<T> speichern

Wie oben schon kurz erwähnt: Standard-Typen können Hashes und Gleichheit schon richtig berechnen.

Aber wenn du z.B. User als Objekte speichern willst, musst du den Vergleich nach einem bestimmten Kriterium (z.B. Login) sicherstellen:

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();
    }
}

// Jetzt geht's:
var users = new HashSet<User>();
users.Add(new User { Login = "vasya" });
users.Add(new User { Login = "petya" });
users.Add(new User { Login = "vasya" }); // Wird nicht hinzugefügt!

Ohne Overrides von Equals und GetHashCode würde HashSet alle Instanzen als verschieden ansehen (auch wenn der Login gleich ist), weil standardmäßig die Speicheradressen verglichen werden.

1
Umfrage/Quiz
Überblick über die wichtigsten Kollektionen, Level 27, Lektion 4
Nicht verfügbar
Überblick über die wichtigsten Kollektionen
Kollektionstypen und Generics
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION