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í return
a 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 IDisposable
a 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ů.