Iterátory

Téměř každý program, který napíšete, bude potřebovat iterovat kolekci. Napíšete kód, který zkoumá každou položku v kolekci.

Vytvoříte také metody iterátoru, což jsou metody, které vytvářejí iterátor pro prvky této třídy. Iterátor je objekt, který prochází kontejnerem, zejména seznamy. Iterátory lze použít pro:

  • Provedení akce pro každou položku v kolekci
  • Vytvoření výčtu vlastní kolekce
  • Rozšíření LINQ nebo jiných knihoven
  • Vytvoření datového kanálu, ve kterém data procházejí efektivně metodami iterátoru.

Jazyk C# poskytuje funkce pro generování i využívání sekvencí. Tyto sekvence lze vytvořit a využívat synchronně nebo asynchronně. Tento článek obsahuje přehled těchto funkcí.

Iterace s foreachem

Vytvoření výčtu kolekce je jednoduché: Klíčové foreach slovo vyčíslí kolekci a spustí vložený příkaz jednou pro každý prvek v kolekci:

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

To je všechno. Pokud chcete iterovat veškerý obsah kolekce, je příkaz foreach vše, co potřebujete. Výrok foreach ale není magický. Spoléhá na dvě obecná rozhraní definovaná v knihovně .NET Core k vygenerování kódu potřebného k iteraci kolekce: IEnumerable<T> a IEnumerator<T>. Tento mechanismus je podrobněji vysvětlen níže.

Obě tato rozhraní mají také ne generické protějšky: IEnumerable a IEnumerator. Obecné verze jsou upřednostňované pro moderní kód.

Pokud je sekvence generována asynchronně, můžete příkaz await foreach použít k asynchronnímu využívání sekvence:

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

Při sekvenci použijete System.Collections.Generic.IEnumerable<T>foreach. Při sekvenci použijete System.Collections.Generic.IAsyncEnumerable<T>await foreach. V druhém případě se sekvence generuje asynchronně.

Výčtové zdroje s metodami iterátoru

Další skvělou funkcí jazyka C# umožňuje vytvářet metody, které vytvářejí zdroj pro výčet. Tyto metody se označují jako metody iterátoru. Metoda iterátoru definuje, jak vygenerovat objekty v posloupnosti při vyžádání. yield return Pomocí kontextových klíčových slov můžete definovat metodu iterátoru.

Tuto metodu byste mohli napsat, abyste vytvořili posloupnost celých čísel od 0 do 9:

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

Výše uvedený kód zobrazuje odlišné yield return příkazy, které zvýrazňují skutečnost, že v metodě iterátoru můžete použít více samostatných yield return příkazů. Pomocí jiných jazykových konstruktorů můžete (a často to udělat) ke zjednodušení kódu metody iterátoru. Následující definice metody vytvoří přesně stejnou sekvenci čísel:

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

Nemusíte se rozhodnout o jednom nebo druhém. Pro splnění potřeb vaší metody můžete mít libovolný počet yield return příkazů:

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

    yield return 50;

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

Všechny tyto předchozí příklady by měly asynchronní protějšek. V každém případě byste nahradili návratový typ IEnumerable<T> .IAsyncEnumerable<T> Předchozí příklad by například měl následující asynchronní verzi:

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++;
}

To je syntaxe synchronních i asynchronních iterátorů. Pojďme se podívat na skutečný příklad. Imagine jste v projektu IoT a senzory zařízení generují velmi velký datový proud. Pokud chcete získat pocit pro data, můžete napsat metodu, která vzorkuje každý Nth datový prvek. Tato malá metoda iterátoru dělá trik:

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

Pokud čtení ze zařízení IoT vytvoří asynchronní sekvenci, upravíte metodu, jak ukazuje následující metoda:

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

Existuje jedno důležité omezení metod iterátoru: nemůžete mít return příkaz i yield return příkaz ve stejné metodě. Následující kód se nezkompiluje:

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

Toto omezení obvykle není problém. Máte možnost použít yield return buď v celé metodě, nebo oddělit původní metodu na více metod, některé pomocí returna některé pomocí yield return.

Poslední metodu můžete mírně upravit tak, aby se používala yield return všude:

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

Někdy je správnou odpovědí rozdělení metody iterátoru do dvou různých metod. Jeden, který používá return, a sekundu, která používá yield return. Představte si situaci, kdy můžete chtít vrátit prázdnou kolekci nebo prvních pět lichých čísel na základě logického argumentu. Můžete napsat tyto dvě metody:

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++;
    }
}

Podívejte se na výše uvedené metody. První používá standardní return příkaz k vrácení prázdné kolekce nebo iterátoru vytvořeného druhou metodou. Druhá metoda používá příkaz yield return k vytvoření požadované sekvence.

Hlouběji se ponořte do foreach

Příkaz foreach se rozšiřuje na standardní idiom, který používá IEnumerable<T> a IEnumerator<T> rozhraní k iteraci napříč všemi prvky kolekce. Minimalizuje také chyby, které vývojáři dělají tím, že prostředky nespravují správně.

Kompilátor přeloží smyčku foreach zobrazenou v prvním příkladu na něco podobného jako v tomto konstruktoru:

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

Přesný kód vygenerovaný kompilátorem je složitější a zpracovává situace, kdy objekt vrácený implementací GetEnumerator()IDisposable rozhraní. Úplné rozšíření vygeneruje kód podobně jako tento:

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

Kompilátor přeloží první asynchronní ukázku do něčeho podobného tomuto konstruktoru:

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

Způsob, jakým je enumerátor odstraněn, závisí na vlastnostech typu enumerator. V obecném synchronním případě finally se klauzule rozšíří na:

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

Obecný asynchronní případ se rozšíří na:

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

Pokud je však enumerator typ zapečetěného typu a neexistuje žádný implicitní převod z typu enumerator na IDisposable nebo IAsyncDisposable, finally klauzule se rozšíří na prázdný blok:

finally
{
}

Pokud existuje implicitní převod z typu enumerator na IDisposablea enumerator je typ hodnoty bez hodnoty null, finally klauzule se rozbalí na:

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

Naštěstí si nemusíte pamatovat všechny tyto podrobnosti. Tento foreach příkaz se stará o všechny drobné odlišnosti za vás. Kompilátor vygeneruje správný kód pro některý z těchto konstruktorů.