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.
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 foreach
u . Wanneer een reeks een System.Collections.Generic.IAsyncEnumerable<T>is, gebruikt await foreach
u . In het laatste geval wordt de reeks asynchroon gegenereerd.
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 return
en 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 return
van , 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.
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 IDisposable
en 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.
.NET-feedback
.NET is een open source project. Selecteer een koppeling om feedback te geven: