CodeGym /Kurse /C# SELF /Enumerierbarkeitsvertrag: ...

Enumerierbarkeitsvertrag: IEnumerable<T>

C# SELF
Level 28 , Lektion 1
Verfügbar

1. Einführung

Wie dir vielleicht schon aufgefallen ist, haben verschiedene C#-Collections gemeinsame Eigenschaften. Zum Beispiel können wir fast jede von ihnen mit einer foreach-Schleife durchlaufen:


var names = new List<string> { "Anja", "Boris", "Vika" };

// Achtung — der Code funktioniert mit List, Array und sogar mit HashSet!
foreach (var name in names)
{
    Console.WriteLine(name);
}

Wo ist die Magie? Das Geheimnis ist, dass alle modernen Collections das Interface IEnumerable<T> implementieren – eine Art "Vertrag", der garantiert, dass die Collection ihre Elemente nacheinander zurückgeben kann.

Stell dir eine Firma vor, in der für jede Mitarbeitersammlung (egal ob Abteilung, Projektteam oder Teilnehmerliste der Firmenfeier) die Regel gilt: Wenn ein Objekt das Interface "IEnumerable" implementiert, kannst du immer alle Mitarbeiter in einer bestimmten Reihenfolge durchgehen – egal, wie sie gespeichert sind, Hauptsache, man kann sie durchlaufen.

2. Interface IEnumerable<T> – was steckt drin?

Schauen wir uns an, wie dieses Interface in .NET aussieht:


public interface IEnumerable<out T>
{
    IEnumerator<T> GetEnumerator();
}

Es gibt nur eine MethodeGetEnumerator, die ein Objekt vom Typ IEnumerator<T> zurückgibt. Dieses Objekt ist für den eigentlichen "Durchlauf" zuständig: Es weiß, welches Element gerade dran ist, wie man zum nächsten kommt und wann Schluss ist.

Kurz gesagt: Wenn eine Klasse (oder Collection) IEnumerable<T> implementiert, kannst du sie mit foreach durchlaufen und die Elemente nacheinander bekommen – egal, wie sie intern aufgebaut ist: als Liste, Hash-Tabelle oder sogar auf der Festplatte.

Mini-Skizze


Collection (z.B. List<T>)
    |
    v
IEnumerable<T>
    |
    v
IEnumerator<T> (der eigentliche "Durchläufer" der Elemente)

3. Praxis und Universalität des Codes

Ein riesiger Vorteil dieses Vertrags ist die Universalität. Wenn eine Funktion oder Methode ein IEnumerable<T> als Parameter nimmt, ist sie mit jedem Collection-Typ kompatibel – von Arrays und Listen bis zu HashSets, Queues und sogar eigenen Collections!

Schauen wir uns zum Beispiel eine Funktion an, die die Summe der Elemente berechnet:


// Du kannst die Summe jeder int-Sammlung berechnen, die IEnumerable<int> unterstützt
int Sum(IEnumerable<int> numbers)
{
    int result = 0;
    foreach (var n in numbers)
        result += n;
    return result;
}

// Funktioniert mit List<int>
var list = new List<int> { 1, 2, 3 };
Console.WriteLine(Sum(list));

// Funktioniert mit Array!
int[] array = { 4, 5, 6 };
Console.WriteLine(Sum(array));

// Funktioniert sogar mit dem Ergebnis einer Methode, die Elemente filtert
Console.WriteLine(Sum(list.Where(x => x % 2 == 0))); // LINQ im Einsatz

Beispiel aus dem Alltag: universelle Methoden

Stell dir vor, du musst ein Tool schreiben, das das längste Wort in einer beliebigen Sammlung von Strings findet. Dank IEnumerable<string> kannst du das für jede Quelle machen – egal ob Array, Liste, Filter-Ergebnis usw.:


string FindLongest(IEnumerable<string> words)
{
    string longest = "";
    foreach (var word in words)
        if (word.Length > longest.Length)
            longest = word;
    return longest;
}

Du kannst das mit jedem passenden Set verwenden.

4. Warum funktioniert foreach mit IEnumerable<T>?

Oft gefragt: Warum "versteht" die foreach-Schleife eigentlich alle Collections? Die Antwort ist einfach: Der C#-Compiler sucht in der Klasse nach der Methode GetEnumerator und erwartet ein Objekt mit den Methoden MoveNext() und der Eigenschaft Current. Das ist das Standardinterface – IEnumerator<T>.

Dank dieses Ansatzes kannst du sogar deine eigene Klasse schreiben, die Elemente nacheinander "ausspuckt" (zum Beispiel einen Fibonacci-Zahlen-Generator), und wenn sie IEnumerable<int> implementiert, kannst du sie mit foreach genauso nutzen wie eine normale Liste.

5. Was ist ein Enumerator und wie funktioniert er?

In jeder Collection, die IEnumerable<T> implementiert, steckt ein spezieller "Durchläufer" – der Enumerator (oder wissenschaftlich: ein Objekt, das das Interface IEnumerator<T> implementiert). Genau der "holt" die Elemente der Collection einzeln raus, wenn du eine foreach-Schleife schreibst.

Interface IEnumerator<T>

Das kann ein Standard-Enumerator:


public interface IEnumerator<T> : IDisposable
{
    T Current { get; }         // Aktuelles Element
    bool MoveNext();           // Zum nächsten Element gehen
    void Reset();              // Zum Anfang zurück (wird selten benutzt)
}

Wie läuft das intern ab?

  • MoveNext() – verschiebt den "Zeiger" zum nächsten Element und gibt true zurück, wenn es noch ein Element gibt. Wenn nicht, gibt es false zurück.
  • Current – gibt das aktuelle Element zurück (das, auf das der Enumerator gerade zeigt).
  • Reset() – setzt den Enumerator auf den Anfang zurück (wird aber fast nie benutzt).
  • Dispose() – gibt Ressourcen frei (wichtig für Collections, die mit Dateien oder dem Netzwerk arbeiten).

Beispiel: Wie funktioniert foreach "unter der Haube"

Wenn du schreibst:


var numbers = new List<int> { 1, 2, 3 };
foreach (var n in numbers)
    Console.WriteLine(n);

Wandelt der Compiler das eigentlich ungefähr so um:


var numbers = new List<int> { 1, 2, 3 };

// Wir holen uns den "Durchläufer"
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
    var n = enumerator.Current;
    Console.WriteLine(n);
}
// Der Compiler ruft automatisch Dispose() im using-Block auf (wenn enumerator IDisposable implementiert)

Wichtig: Wenn die Collection mit externen Ressourcen arbeitet (z.B. Dateien, Datenbanken), kann der Enumerator diese automatisch freigeben, wenn der Durchlauf fertig ist.

Visuelle Skizze


Start des Durchlaufs -> GetEnumerator() -> Enumerator
         |
         v
  MoveNext() -> Current
         |
         v
  MoveNext() -> Current
         |
        ...
         |
         v
  MoveNext() == false -> Ende des Durchlaufs

6. IEnumerable und Arrays, Listen, Sets: Wer ist mit wem verwandt?

Schauen wir uns an, welche Standard-Container in .NET dieses Interface implementieren:

Collection-Typ Implementiert IEnumerable<T>? Mit foreach durchlaufen?
Array (
int[]
)
List<T>
Dictionary<TKey, V>
✅ (über Paare, Keys, Values)
HashSet<T>
Queue<T>
Stack<T>

Sogar der String (string) implementiert das normale IEnumerable, also kannst du auch jeden Buchstaben einer Zeichenkette so durchlaufen.

7. Eigene Enumerable implementieren

Coole Aufgabe: Lass uns mal eine minimale Collection bauen, die gerade Zahlen von 0 bis N speichert und IEnumerable<int> implementiert. Dann können wir sie mit foreach durchlaufen und mit LINQ nutzen.


// Collection-Klasse, die man "durchlaufen" kann
class EvenNumbers : IEnumerable<int>
{
    private int max;

    public EvenNumbers(int max)
    {
        this.max = max;
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i <= max; i += 2)
            yield return i; // Spezielle Magie für die Enumerator-Implementierung
    }

    // Explizite Implementierung des nicht-generischen IEnumerable für Kompatibilität
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

// Verwendung:
var evens = new EvenNumbers(10);
foreach(var e in evens)
    Console.Write($"{e} "); // 0 2 4 6 8 10

Wichtiger Punkt: Wenn deine Klasse IEnumerable<T> implementiert, ist sie automatisch mit den meisten .NET-Tools kompatibel: LINQ, foreach, Methoden, die ein Enumerable erwarten.

8. Typische Fehler und Besonderheiten

Manche Anfänger denken, dass IEnumerable<T> eine eigene Collection ist, die Elemente enthält. In Wirklichkeit ist es nur ein "Versprechen", dass beim Durchlaufen die Elemente einzeln "geliefert" werden.

Wenn du zufälligen Zugriff per Index brauchst (myList[5]), Methoden wie Add oder Remove nutzen willst – das geht mit IEnumerable<T> nicht. Das ist nur für sequentielles Durchlaufen!

Fehler: Die Collection während des Durchlaufs verändern. Zum Beispiel:


foreach (var item in myList)
{
    if (item < 0)
        myList.Remove(item); // GEFÄHRLICH! InvalidOperationException
}

Besser ist es, zuerst eine separate Liste für das Löschen zu bauen oder Methoden zu nutzen, die eine neue Collection erzeugen:


// Sicherer Weg – wir bauen eine neue Collection manuell
var newList = new List<int>();
foreach (var item in myList)
{
    if (item >= 0)
        newList.Add(item);
}
myList = newList;

// Oder wir löschen per Index in umgekehrter Reihenfolge
for (int i = myList.Count - 1; i >= 0; i--)
{
    if (myList[i] < 0)
        myList.RemoveAt(i);
}
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION