Iterators

Bijna elk programma dat u schrijft, moet een verzameling herhalen. U schrijft code waarmee elk item in een verzameling wordt onderzocht.

U maakt ook iteratormethoden. Dit zijn methoden die een iterator produceren voor de elementen van die klasse. Een iterator is een object dat een container doorkruist, met name lijsten. Iterators kunnen worden gebruikt voor:

  • Een actie uitvoeren op elk item in een verzameling.
  • Een aangepaste verzameling inventariseren.
  • LINQ of andere bibliotheken uitbreiden.
  • Een gegevenspijplijn maken waarbij gegevens efficiënt stromen via iteratormethoden.

De C#-taal biedt functies voor het genereren en gebruiken van reeksen. Deze reeksen kunnen synchroon of asynchroon worden geproduceerd en verbruikt. Dit artikel bevat een overzicht van deze functies.

Herhalen met foreach

Het inventariseren van een verzameling is eenvoudig: het foreach trefwoord inventariseert een verzameling, waarbij de ingesloten instructie eenmaal wordt uitgevoerd voor elk element in de verzameling:

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

Dat is alles. Als u alle inhoud van een verzameling wilt herhalen, hebt u alleen de foreach instructie nodig. De foreach uitspraak is echter geen magie. Het is afhankelijk van twee algemene interfaces die zijn gedefinieerd in de .NET Core-bibliotheek om de code te genereren die nodig is om een verzameling te herhalen: IEnumerable<T> en IEnumerator<T>. Dit mechanisme wordt hieronder uitgebreid beschreven.

Beide interfaces hebben ook niet-generieke tegenhangers: IEnumerable en IEnumerator. De algemene versies hebben de voorkeur voor moderne code.

Wanneer een reeks asynchroon wordt gegenereerd, kunt u de await foreach instructie gebruiken om de reeks asynchroon te gebruiken:

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

Wanneer een reeks een System.Collections.Generic.IEnumerable<T>is, gebruikt foreachu . Wanneer een reeks een System.Collections.Generic.IAsyncEnumerable<T>is, gebruikt await foreachu . In het laatste geval wordt de reeks asynchroon gegenereerd.

Opsommingsbronnen met iteratormethoden

Een andere handige functie van de C#-taal stelt u in staat om methoden te bouwen waarmee een bron voor een opsomming wordt gemaakt. Deze methoden worden iteratormethoden genoemd. Een iterator-methode definieert hoe de objecten in een reeks worden gegenereerd wanneer dit wordt aangevraagd. U gebruikt de yield return contextuele trefwoorden om een iteratormethode te definiëren.

U kunt deze methode schrijven om de reeks gehele getallen van 0 tot en met 9 te produceren:

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

De bovenstaande code toont afzonderlijke yield return instructies om het feit te markeren dat u meerdere discrete yield return instructies in een iterator-methode kunt gebruiken. U kunt (en vaak) andere taalconstructies gebruiken om de code van een iterator-methode te vereenvoudigen. De onderstaande methodedefinitie produceert exact dezelfde reeks getallen:

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

U hoeft niet een of de andere te beslissen. U kunt zoveel yield return instructies hebben als nodig is om te voldoen aan de behoeften van uw methode:

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

    yield return 50;

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

Al deze voorgaande voorbeelden zouden een asynchrone tegenhanger hebben. In elk geval vervangt u het retourtype door IEnumerable<T> een IAsyncEnumerable<T>. Het vorige voorbeeld zou bijvoorbeeld de volgende asynchrone versie hebben:

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

Dit is de syntaxis voor zowel synchrone als asynchrone iterators. Laten we eens kijken naar een praktijkvoorbeeld. Stel dat u een IoT-project gebruikt en dat de apparaatsensoren een zeer grote gegevensstroom genereren. Als u een idee wilt krijgen van de gegevens, kunt u een methode schrijven waarmee elk Nth-gegevenselement wordt gebruikt. Deze kleine iteratormethode doet de truc:

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

Als het lezen van het IoT-apparaat een asynchrone reeks produceert, wijzigt u de methode zoals in de volgende methode wordt weergegeven:

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

Er is één belangrijke beperking voor iteratormethoden: u kunt niet zowel een return instructie als een yield return instructie in dezelfde methode hebben. De volgende code wordt niet gecompileerd:

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

Deze beperking is normaal gesproken geen probleem. U kunt kiezen uit het gebruik yield return van de hele methode of het scheiden van de oorspronkelijke methode in meerdere methoden, sommige met returnen sommige met behulp van yield return.

U kunt de laatste methode enigszins wijzigen om overal te gebruiken 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;
}

Soms is het juiste antwoord het splitsen van een iteratormethode in twee verschillende methoden. Een die gebruikmaakt returnvan , en een seconde die gebruikmaakt van yield return. Overweeg een situatie waarin u mogelijk een lege verzameling of de eerste vijf oneven getallen wilt retourneren, op basis van een booleaanse argument. U kunt dit schrijven als deze twee methoden:

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

Bekijk de bovenstaande methoden. De eerste gebruikt de standaardinstructie return om een lege verzameling te retourneren of de iterator die door de tweede methode is gemaakt. De tweede methode gebruikt de yield return instructie om de aangevraagde reeks te maken.

Dieper ingaan op foreach

De foreach instructie wordt uitgebreid naar een standaardidioom die gebruikmaakt van de IEnumerable<T> en IEnumerator<T> interfaces om alle elementen van een verzameling te herhalen. Het minimaliseert ook fouten die ontwikkelaars maken door resources niet goed te beheren.

De compiler vertaalt de foreach lus die in het eerste voorbeeld wordt weergegeven in iets vergelijkbaars met deze constructie:

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

De exacte code die door de compiler wordt gegenereerd, is ingewikkelder en verwerkt situaties waarin het object dat wordt geretourneerd door GetEnumerator() de IDisposable interface wordt geïmplementeerd. Met de volledige uitbreiding wordt code meer als volgt gegenereerd:

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

De compiler vertaalt het eerste asynchrone voorbeeld in iets vergelijkbaars met deze constructie:

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

De manier waarop de enumerator wordt verwijderd, is afhankelijk van de kenmerken van het type enumerator. In het algemene synchrone geval wordt de finally component uitgebreid naar:

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

Het algemene asynchrone geval wordt uitgebreid naar:

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

Als het type echter een verzegeld type enumerator is en er geen impliciete conversie is van het type enumerator van naar IDisposable of IAsyncDisposable, wordt de finally component uitgebreid naar een leeg blok:

finally
{
}

Als er een impliciete conversie is van enumerator het type van naar IDisposableen enumerator een niet-null-waardetype is, wordt de finally component uitgebreid naar:

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

Gelukkig hoeft u al deze details niet te onthouden. De foreach instructie verwerkt al die nuances voor u. De compiler genereert de juiste code voor een van deze constructies.