Iterátory

Téměř každý program, který napíšete, bude potřebovat iterovat přes kolekci. Napíšete kód, který prozkoumá všechny položky 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 pomocí foreachu

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. K iteraci veškerého obsahu kolekce stačí příkaz foreach . Ten foreach výrok 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 se sekvence generuje asynchronně, můžete pomocí await foreach příkazu asynchronně využívat sekvenci:

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

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

Výčtové zdroje s metodami iterátoru

Další skvělá funkce 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 sekvenci při vyžádání. Kontextová klíčová slova slouží yield return k definování metody iterátoru.

Tuto metodu můžete napsat, abyste vytvořili sekvenci 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 diskrétních yield return příkazů. K zjednodušení kódu metody iterátoru můžete (a často také použít) konstruktory jiného jazyka. 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ý IEnumerable<T> typ znakem IAsyncEnumerable<T>. Předchozí příklad by měl například 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. Představte si, že používáte projekt IoT a senzory zařízení generují velmi velký datový proud. Pokud chcete získat pocit z dat, můžete napsat metodu, která vzorkuje každý Nth datový prvek. Tato malá metoda iterátoru provede 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 příkaz return 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 buď použít yield return v rámci této metody, nebo oddělit původní metodu do 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;
}

V některých případech je správnou odpovědí rozdělení metody iterátoru na dvě různé metody. 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 pomocí yield return příkazu vytvoří požadovanou sekvenci.

Ponořte se hlouběji do foreach

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

Kompilátor přeloží smyčku foreach zobrazenou v prvním příkladu do něčeho podobného jako tento konstruktor:

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ý GetEnumerator() implementací 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 uvolně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 rozbalí na:

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

Pokud je však typ enumerator 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 nenulový typ hodnoty, finally klauzule se rozšíří 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 ty drobné odlišnosti za vás. Kompilátor vygeneruje správný kód pro některý z těchto konstruktorů.