Iterátorok

Szinte minden megírt programnak meg kell iterálnia egy gyűjteményt. Olyan kódot fog írni, amely egy gyűjtemény minden elemét megvizsgálja.

Iterátor metódusokat is létrehozhat, amelyek olyan metódusok, amelyek iterátort hoznak létre az adott osztály elemeihez. Az iterátor egy olyan objektum, amely bejár egy tárolót, különösen a listákat. Az iterátorok a következő célokra használhatók:

  • Művelet végrehajtása a gyűjtemény minden elemén.
  • Egyéni gyűjtemény számbavétele.
  • A LINQ vagy más kódtárak kiterjesztése.
  • Olyan adatfolyam létrehozása, amelyben az adatok iterátori metódusokkal hatékonyan haladnak.

A C#-nyelv funkciókkal rendelkezik a sorozatok létrehozásához és felhasználásához is. Ezek a sorozatok szinkron vagy aszinkron módon hozhatók létre és használhatók fel. Ez a cikk áttekintést nyújt ezekről a funkciókról.

Iterálás foreach használatával

A gyűjtemények számbavétele egyszerű: A foreach kulcsszó számba ad egy gyűjteményt, és egyszer végrehajtja a beágyazott utasítást a gyűjtemény minden eleméhez:

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

Ennyi az egész. Ha egy gyűjtemény összes tartalmát át szeretné iterálni, csak az foreach utasításra van szüksége. Az foreach állítás nem varázslat. A .NET core-kódtárban definiált két általános adapterre támaszkodik a gyűjtemény iterálásához szükséges kód létrehozásához: IEnumerable<T> és IEnumerator<T>. Ezt a mechanizmust az alábbiakban részletesebben ismertetjük.

Mindkét interfész nem általános megfelelőkkel is rendelkezik: IEnumerable és IEnumerator. A modern kódhoz az általános verziókat részesítik előnyben.

Ha egy sorozat aszinkron módon jön létre, az await foreach utasítással aszinkron módon használhatja fel a sorozatot:

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

Ha egy sorozat egy System.Collections.Generic.IEnumerable<T>, akkor azt kell használni foreach. Ha egy sorozat egy System.Collections.Generic.IAsyncEnumerable<T>, akkor azt kell használni await foreach. Az utóbbi esetben a szekvencia aszinkron módon jön létre.

Enumerálási források iterációs metódusokkal

A C# nyelv egy másik nagyszerű funkciója lehetővé teszi az enumerálás forrását létrehozó metódusok létrehozását. Ezeket a metódusokat iterátor metódusoknak nevezzük. Az iterátormetódus meghatározza, hogyan hozhatja létre az objektumokat egy sorrendben, amikor az szükséges. A környezetfüggő kulcsszavak használatával yield return definiálhat egy iterátormetódust.

Ezt a metódust úgy is megírhatja, hogy az egész számok sorozata 0 és 9 között legyen:

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

A fenti kód különböző yield return utasításokat jelenít meg, amelyek kiemelik azt a tényt, hogy több különálló yield return utasítást is használhat egy iterátor-metódusban. Más nyelvi szerkezetekkel is leegyszerűsítheti az iterátor metódus kódját. Az alábbi metódusdefiníció pontosan ugyanazt a számsort állítja elő:

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

Nem kell egyiket vagy a másikat választania. A metódus igényeinek kielégítéséhez annyi utasítást használhat yield return , amennyi szükséges:

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

    yield return 50;

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

A fenti példák mindegyike aszinkron megfelelővel rendelkezik. Minden esetben lecseréli a visszatérési típust IEnumerable<T> egy IAsyncEnumerable<T>. Az előző példa például a következő aszinkron verzióval rendelkezik:

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

Ez a szinkron és az aszinkron iterátorok szintaxisa. Tekintsünk egy valós példát. Tegyük fel, hogy egy IoT-projekten dolgozik, és az eszközérzékelők nagyon nagy adatstreamet hoznak létre. Ha meg szeretné érezni az adatokat, írhat egy metódust, amely minden Nth adatelemet mintát vesz. Ez a kis iterátor módszer elvégzi a trükköt:

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

Ha az IoT-eszközről történő olvasás aszinkron sorozatot hoz létre, a metódust az alábbi módszer szerint módosíthatja:

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

Az iterátormetódusokra egyetlen fontos korlátozás vonatkozik: ugyanabban a metódusban nem lehet utasítást és utasítást yield return is return használni. A következő kód nem lesz lefordítva:

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

Ez a korlátozás általában nem jelent problémát. Választhat, hogy az egész metódust használja yield return , vagy az eredeti metódust több metódusra, néhányat használ return, másokat pedig használ yield return.

Az utolsó metódust kismértékben módosíthatja, hogy mindenhol használhassa 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;
}

Néha a helyes válasz az iterátor metódus két különböző módszerre való felosztása. Az egyiket használjareturn, a másikat pedig a másodikat.yield return Vegyük figyelembe azt a helyzetet, amikor egy logikai argumentum alapján üres gyűjteményt vagy az első öt páratlan számot szeretnénk visszaadni. A következő két módszerként írhatja ezt:

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

Tekintse meg a fenti módszereket. Az első a standard return utasítással egy üres gyűjteményt, vagy a második metódus által létrehozott iterátort ad vissza. A második metódus az yield return utasítást használja a kért sorozat létrehozásához.

Részletesebben is megismerkedünk foreach

Az foreach utasítás egy szabványos kifejezéssé bővül, amely a gyűjtemény összes elemének iterálására használja az IEnumerable<T> és IEnumerator<T> a felületeket. Emellett az erőforrások nem megfelelő kezelésével minimalizálja a fejlesztők által tapasztalt hibákat.

A fordító az foreach első példában látható hurkot az alábbi szerkezethez hasonlóra fordítja le:

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

A fordító által létrehozott pontos kód bonyolultabb, és kezeli azokat a helyzeteket, amikor az interfész által visszaadott GetEnumerator() objektum implementálja az interfészt IDisposable . A teljes bővítés a következőhöz hasonló kódot hoz létre:

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

A fordító az első aszinkron mintát az alábbi szerkezethez hasonlóra fordítja le:

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

Az enumerátor elidegenítésének módjától függ a típus enumeratorjellemzőitől. Az általános szinkron esetben a záradék a finally következőre bővül:

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

Az általános aszinkron eset a következőre bővül:

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

Ha azonban a típus enumerator egy lezárt típus, és nem történik implicit átalakítás a enumeratorIDisposable típusról vagy IAsyncDisposablea típusra, a finally záradék üres blokktá bővül:

finally
{
}

Ha implicit átalakítás történik a típusról enumeratorIDisposablea , és enumerator nem null értékű értékre, a záradék a finally következőre bővül:

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

Szerencsére nem kell emlékeznie ezekre a részletekre. Az foreach utasítás kezeli az összes árnyalatot. A fordító létrehozza a megfelelő kódot ezen szerkezetek bármelyikéhez.