Freigeben über


Iteratoren

Bei fast jedem Programm, das Sie schreiben, muss eine Auflistung durchlaufen werden. Sie schreiben Code, der jedes Element in einer Auflistung untersucht.

Außerdem erstellen Sie Iteratormethoden, bei denen es sich um Methoden handelt, die einen Iterator für die Elemente dieser Klasse erzeugen. Ein Iterator ist ein Objekt, das einen Container durchläuft, insbesondere Listen. Iteratoren können verwendet werden für:

  • Ausführen einer Aktion für jedes Element in einer Auflistung.
  • Enumerieren einer benutzerdefinierten Auflistung
  • Erweitern von LINQ oder anderen Bibliotheken
  • Erstellen einer Datenpipeline, in der Daten effizient durch Iteratormethoden fließen.

Die C#-Sprache bietet Features zum Generieren und Verwenden von Sequenzen. Diese Sequenzen können synchron oder asynchron produziert und genutzt werden. Dieser Artikel enthält eine Übersicht über diese Features.

Durchlaufen mit foreach

Das Aufzählen einer Auflistung ist einfach: Das foreach Schlüsselwort listet eine Auflistung auf und führt die eingebettete Anweisung einmal für jedes Element in der Auflistung aus:

foreach (var item in collection)
{
    Console.WriteLine(item?.ToString());
}

Das ist alles. Für das Durchlaufen aller Inhalte einer Auflistung benötigen Sie nur die foreach-Anweisung. Die foreach Aussage ist jedoch nicht zauberhaft. Es basiert auf zwei generischen Schnittstellen, die in der .NET-Kernbibliothek definiert sind, um den Code zu generieren, der zum Durchlaufen einer Auflistung erforderlich ist: IEnumerable<T> und IEnumerator<T>. Dieser Mechanismus wird unten ausführlicher erläutert.

Beide Schnittstellen weisen auch nicht generische Gegensätze auf: IEnumerable und IEnumerator. Die generischen Versionen werden für modernen Code bevorzugt.

Wenn eine Sequenz asynchron generiert wird, können Sie die await foreach Anweisung verwenden, um die Sequenz asynchron zu nutzen:

await foreach (var item in asyncSequence)
{
Console.WriteLine(item?.ToString());
}

Wenn eine Sequenz eine System.Collections.Generic.IEnumerable<T> ist, verwenden Sie foreach. Wenn eine Sequenz eine System.Collections.Generic.IAsyncEnumerable<T> ist, verwenden Sie await foreach. In letzterem Fall wird die Sequenz asynchron generiert.

Enumerationsquellen mit Iteratormethoden

Ein weiteres großartiges Feature der C#-Sprache ermöglicht es Ihnen, Methoden zu erstellen, die eine Quelle für eine Enumeration erstellen. Diese Methoden werden als Iteratormethoden bezeichnet. Eine Iteratormethode definiert, wie die Objekte bei Bedarf in einer Sequenz generiert werden. Sie verwenden die yield return Kontextstichwörter, um eine Iteratormethode zu definieren.

Sie können diese Methode schreiben, um die Sequenz von ganzen Zahlen zwischen 0 und 9 zu erzeugen:

public IEnumerable<int> GetSingleDigitNumbers()
{
    yield return 0;
    yield return 1;
    yield return 2;
    yield return 3;
    yield return 4;
    yield return 5;
    yield return 6;
    yield return 7;
    yield return 8;
    yield return 9;
}

Der obige Code zeigt unterschiedliche yield return Anweisungen, um die Tatsache hervorzuheben, dass Sie mehrere diskrete yield return Anweisungen in einer Iteratormethode verwenden können. Sie können (und häufig) andere Sprachkonstrukte verwenden, um den Code einer Iteratormethode zu vereinfachen. Die folgende Methodendefinition erzeugt genau dieselbe Sequenz von Zahlen:

public IEnumerable<int> GetSingleDigitNumbersLoop()
{
    int index = 0;
    while (index < 10)
        yield return index++;
}

Sie müssen sich nicht für eine oder die andere entscheiden. Sie können so viele yield return-Anweisungen verwenden wie für Ihre Methode erforderlich:

public IEnumerable<int> GetSetsOfNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    index = 100;
    while (index < 110)
        yield return index++;
}

Alle diese vorstehenden Beispiele hätten ein asynchrones Gegenstück. In jedem Fall ersetzen Sie den Rückgabetyp IEnumerable<T> durch einen IAsyncEnumerable<T>. Das vorherige Beispiel würde beispielsweise die folgende asynchrone Version aufweisen:

public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    await Task.Delay(500);

    yield return 50;

    await Task.Delay(500);

    index = 100;
    while (index < 110)
        yield return index++;
}

Dies ist die Syntax für synchrone und asynchrone Iteratoren. Betrachten wir ein Beispiel aus der realen Welt. Stellen Sie sich vor, Sie befinden sich in einem IoT-Projekt, und die Gerätesensoren generieren einen sehr großen Datenstrom. Um ein Gefühl für die Daten zu bekommen, können Sie eine Methode schreiben, die bei jedem n-ten Datenelement eine Stichprobe durchführt. Diese kleine Iteratormethode macht den Trick:

public static IEnumerable<T> Sample<T>(this IEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

Wenn das Lesen vom IoT-Gerät eine asynchrone Sequenz erzeugt, würden Sie die Methode ändern, wie die folgende Methode zeigt:

public static async IAsyncEnumerable<T> Sample<T>(this IAsyncEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    await foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

Es gibt eine wichtige Einschränkung für Iteratormethoden: Sie können nicht sowohl eine return-Anweisung als auch eine yield return-Anweisung in derselben Methode verwenden. Der folgende Code wird nicht kompiliert:

public IEnumerable<int> GetSingleDigitNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    // generates a compile time error:
    var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
    return items;
}

Diese Einschränkung ist normalerweise kein Problem. Sie haben die Wahl, entweder die gesamte Methode zu verwenden yield return oder die ursprüngliche Methode in mehrere Methoden zu trennen, einige verwenden returnund einige verwenden yield return.

Sie können die letzte Methode leicht ändern, um überall zu verwenden yield return :

public IEnumerable<int> GetFirstDecile()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
    foreach (var item in items)
        yield return item;
}

Manchmal besteht die richtige Antwort darin, eine Iteratormethode in zwei verschiedene Methoden aufzuteilen. Eine, die return verwendet, und eine Zweite, die yield return verwendet. Betrachten Sie eine Situation, in der Sie eine leere Auflistung oder die ersten fünf ungeraden Zahlen basierend auf einem booleschen Argument zurückgeben möchten. Sie könnten dies als diese beiden Methoden schreiben:

public IEnumerable<int> GetSingleDigitOddNumbers(bool getCollection)
{
    if (getCollection == false)
        return new int[0];
    else
        return IteratorMethod();
}

private IEnumerable<int> IteratorMethod()
{
    int index = 0;
    while (index < 10)
    {
        if (index % 2 == 1)
            yield return index;
        index++;
    }
}

Sehen Sie sich die oben beschriebenen Methoden an. Die erste verwendet die Standard-Anweisung return , um entweder eine leere Auflistung oder den von der zweiten Methode erstellten Iterator zurückzugeben. Die zweite Methode verwendet die yield return Anweisung, um die angeforderte Sequenz zu erstellen.

Tieferer Einblick in foreach

Die foreach-Anweisung wird in einen Standardausdruck erweitert, der die Schnittstellen IEnumerable<T> und IEnumerator<T> für das Durchlaufen aller Elemente einer Auflistung verwendet. Außerdem werden Fehler minimiert, die Entwickler vornehmen, indem Ressourcen nicht ordnungsgemäß verwaltet werden.

Der Compiler übersetzt die foreach im ersten Beispiel gezeigte Schleife in ein ähnliches Konstrukt:

IEnumerator<int> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
    var item = enumerator.Current;
    Console.WriteLine(item.ToString());
}

Der vom Compiler generierte genaue Code ist komplizierter und bewältigt Situationen, in denen das von GetEnumerator() zurückgegebene Objekt die IDisposable-Schnittstelle implementiert. Die vollständige Erweiterung generiert Code wie folgt:

{
    var enumerator = collection.GetEnumerator();
    try
    {
        while (enumerator.MoveNext())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    }
    finally
    {
        // dispose of enumerator.
    }
}

Der Compiler übersetzt das erste asynchrone Beispiel in ein ähnliches Konstrukt:

{
    var enumerator = collection.GetAsyncEnumerator();
    try
    {
        while (await enumerator.MoveNextAsync())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    }
    finally
    {
        // dispose of async enumerator.
    }
}

Die Art und Weise, wie der Enumerator verworfen wird, hängt von den Merkmalen des Typs von enumerator ab. Im allgemeinen synchronen Fall erweitert sich die finally Klausel auf:

finally
{
   (enumerator as IDisposable)?.Dispose();
}

Der allgemeine asynchrone Fall wird auf Folgendes erweitert:

finally
{
    if (enumerator is IAsyncDisposable asyncDisposable)
        await asyncDisposable.DisposeAsync();
}

Wenn der Typ enumerator ein versiegelter Typ ist und es keine implizite Konvertierung vom Typ enumerator zu IDisposable oder IAsyncDisposable gibt, wird die finally-Klausel zu einem leeren Block erweitert:

finally
{
}

Wenn eine implizite Konvertierung vom Typ enumerator zu IDisposable existiert und enumerator ein nicht-nullbarer Werttyp ist, wird die finally-Klausel auf Folgendes erweitert:

finally
{
   ((IDisposable)enumerator).Dispose();
}

Glücklicherweise müssen Sie sich nicht alle diese Details merken. Die foreach-Anweisung kümmert sich für Sie um alle diese Nuancen. Der Compiler generiert den richtigen Code für jedes dieser Konstrukte.