Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Prawie każdy program, który napiszesz, będzie musiał przechodzić przez kolekcję. 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 bibliotek, takich jak LINQ.
- 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ć przez wszystkie elementy kolekcji, instrukcja foreach
jest wszystkim, czego potrzebujesz. 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 System.Collections.Generic.IEnumerable<T>, używasz foreach
. Gdy sekwencja jest System.Collections.Generic.IAsyncEnumerable<T>, używasz await foreach
. W tym drugim przypadku sekwencja jest generowana asynchronicznie.
Źródła enumeracji 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. Używasz słów kluczowych kontekstowych yield return
, aby zdefiniować metodę 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ć tyle instrukcji yield return
, ile potrzebujesz.
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ć typ zwracany IEnumerable<T>
typem 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 z życia. 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 rozwiązuje problem.
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 wybrać albo użycie yield return
przez całą metodę, albo rozdzielić oryginalną metodę na wiele metod, z których niektóre będą używać return
, a inne 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 return
, i druga, która używa 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 interfejsów IEnumerable<T>
i IEnumerator<T>
do iterowania po wszystkich elementach 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, gdy obiekt zwracany przez GetEnumerator()
implementuje interfejs 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 IDisposable
, i enumerator
jest typem wartości innej niż null, klauzula finally
rozszerza się na:
finally
{
((IDisposable)enumerator).Dispose();
}
Na szczęście nie musisz pamiętać wszystkich tych szczegółów. Instrukcja foreach
zajmuje się wszystkimi tymi niuansami. Kompilator wygeneruje prawidłowy kod dla dowolnej z tych konstrukcji.