Iteratory
Prawie każdy program, który piszesz, będzie musiał iterować w kolekcji. Napiszesz kod, który analizuje każdy element w kolekcji.
Utworzysz również metody iteracyjne, które są metodami, które tworzą iterator dla elementów tej klasy. Iterator to obiekt, który przechodzi przez kontener, szczególnie listy. Iteratory mogą służyć do:
- Wykonywanie akcji dla każdego elementu w kolekcji.
- Wyliczanie kolekcji niestandardowej.
- Rozszerzanie linQ lub innych bibliotek.
- Tworzenie potoku danych, w którym dane przepływa wydajnie za pośrednictwem metod iteratora.
Język C# udostępnia funkcje zarówno do generowania, jak i używania sekwencji. Sekwencje te mogą być tworzone i zużywane synchronicznie lub asynchronicznie. Ten artykuł zawiera omówienie tych funkcji.
Iterowanie z foreach
Wyliczanie kolekcji jest proste: foreach
słowo kluczowe wylicza kolekcję, wykonując instrukcję osadzoną raz dla każdego elementu w kolekcji:
foreach (var item in collection)
{
Console.WriteLine(item?.ToString());
}
To wszystko. Aby iterować całą zawartość kolekcji, foreach
instrukcja jest potrzebna. Instrukcja foreach
nie jest jednak magią. Opiera się on na dwóch interfejsach ogólnych zdefiniowanych w bibliotece .NET Core w celu wygenerowania kodu niezbędnego do iterowania kolekcji: IEnumerable<T>
i IEnumerator<T>
. Ten mechanizm wyjaśniono bardziej szczegółowo poniżej.
Oba te interfejsy mają również niegeneryczne odpowiedniki: IEnumerable
i IEnumerator
. Wersje ogólne są preferowane dla nowoczesnego kodu.
Gdy sekwencja jest generowana asynchronicznie, możesz użyć await foreach
instrukcji , aby asynchronicznie wykorzystać sekwencję:
await foreach (var item in asyncSequence)
{
Console.WriteLine(item?.ToString());
}
Gdy sekwencja jest elementem System.Collections.Generic.IEnumerable<T>, należy użyć polecenia foreach
. Gdy sekwencja jest elementem System.Collections.Generic.IAsyncEnumerable<T>, należy użyć polecenia await foreach
. W tym drugim przypadku sekwencja jest generowana asynchronicznie.
Źródła wyliczenia z metodami iteratora
Kolejna świetna funkcja języka C# umożliwia tworzenie metod tworzących źródło dla wyliczenia. Te metody są określane jako metody iteracyjne. Metoda iteratora definiuje sposób generowania obiektów w sekwencji po żądaniu. Słowa kluczowe kontekstowe służą yield return
do definiowania metody iteratora.
Tę metodę można napisać, aby utworzyć sekwencję liczb całkowitych z zakresu 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;
}
Powyższy kod pokazuje odrębne yield return
instrukcje, aby podkreślić fakt, że można użyć wielu instrukcji dyskretnych yield return
w metodzie iteratora. Można (i często) używać innych konstrukcji językowych, aby uprościć kod metody iteratora. Poniższa definicja metody tworzy dokładnie tę samą sekwencję liczb:
public IEnumerable<int> GetSingleDigitNumbersLoop()
{
int index = 0;
while (index < 10)
yield return index++;
}
Nie musisz decydować o jednym lub drugim. Aby spełnić potrzeby metody, możesz mieć dowolną liczbę yield return
instrukcji:
public IEnumerable<int> GetSetsOfNumbers()
{
int index = 0;
while (index < 10)
yield return index++;
yield return 50;
index = 100;
while (index < 110)
yield return index++;
}
Wszystkie powyższe przykłady miałyby asynchroniczny odpowiednik. W każdym przypadku należy zastąpić zwracany typ IEnumerable<T>
elementu ciągiem IAsyncEnumerable<T>
. Na przykład w poprzednim przykładzie będzie następująca wersja asynchroniczna:
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++;
}
Jest to składnia zarówno synchronicznych, jak i asynchronicznych iteratorów. Rozważmy rzeczywisty przykład świata. Wyobraź sobie, że korzystasz z projektu IoT, a czujniki urządzeń generują bardzo duży strumień danych. Aby uzyskać informacje o danych, możesz napisać metodę, która próbkuje każdy element danych Nth. Ta mała metoda iteratora wykonuje sztuczkę:
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;
}
}
Jeśli odczyt z urządzenia IoT generuje sekwencję asynchroniczną, zmodyfikuj metodę, jak pokazano w następującej metodzie:
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;
}
}
Istnieje jedno ważne ograniczenie dotyczące metod iteratora: nie można mieć zarówno return
instrukcji, jak i yield return
instrukcji w tej samej metodzie. Następujący kod nie zostanie skompilowany:
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;
}
To ograniczenie zwykle nie jest problemem. Możesz użyć metody yield return
w całej metodzie lub rozdzielić oryginalną metodę na wiele metod, niektóre przy użyciu metody i niektóre przy użyciu return
metody yield return
.
Możesz nieco zmodyfikować ostatnią metodę, aby używać yield return
wszędzie:
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;
}
Czasami właściwą odpowiedzią jest podzielenie metody iteratora na dwie różne metody. Jedna, która używa elementu return
, i drugiego, który używa elementu yield return
. Rozważ sytuację, w której możesz zwrócić pustą kolekcję lub pięć pierwszych nieparzystnych liczb na podstawie argumentu logicznego. Możesz napisać to jako następujące dwie 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++;
}
}
Przyjrzyj się powyższym metodom. Pierwszy używa standardowej return
instrukcji, aby zwrócić pustą kolekcję lub iterator utworzony przez drugą metodę. Druga metoda używa instrukcji , yield return
aby utworzyć żądaną sekwencję.
Dokładniejsze omówienie foreach
Instrukcja foreach
rozszerza się na standardowy idiom, który używa IEnumerable<T>
interfejsów i IEnumerator<T>
do iterowania wszystkich elementów kolekcji. Minimalizuje również błędy, które deweloperzy popełniają, nie prawidłowo zarządzając zasobami.
Kompilator tłumaczy pętlę foreach
pokazaną w pierwszym przykładzie na podobną do tej konstrukcji:
IEnumerator<int> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
Dokładny kod generowany przez kompilator jest bardziej skomplikowany i obsługuje sytuacje, w których obiekt zwracany przez GetEnumerator()
implementację interfejsu IDisposable
. Pełne rozszerzenie generuje kod bardziej podobny do następującego:
{
var enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of enumerator.
}
}
Kompilator tłumaczy pierwszy przykład asynchroniczny na coś podobnego do tej konstrukcji:
{
var enumerator = collection.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of async enumerator.
}
}
Sposób, w jaki moduł wyliczający jest usuwany, zależy od cech typu enumerator
. W ogólnym przypadku synchronicznym klauzula finally
rozszerza się na:
finally
{
(enumerator as IDisposable)?.Dispose();
}
Ogólny przypadek asynchroniczny jest rozszerzany na:
finally
{
if (enumerator is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
}
Jeśli jednak typ enumerator
jest typu zapieczętowanego i nie ma niejawnej konwersji z typu enumerator
na IDisposable
lub IAsyncDisposable
, finally
klauzula rozszerza się do pustego bloku:
finally
{
}
Jeśli istnieje niejawna konwersja z typu enumerator
na , i enumerator
jest typem wartości innej niż null, klauzula finally
IDisposable
rozszerza się na:
finally
{
((IDisposable)enumerator).Dispose();
}
Na szczęście nie musisz pamiętać wszystkich tych szczegółów. Instrukcja foreach
obsługuje wszystkie te niuanse. Kompilator wygeneruje prawidłowy kod dla dowolnej z tych konstrukcji.