C# udostępnia składnię nazwaną iteratorem. Jest to metoda generująca sekwencję, korzystającą ze słowa kluczowego yield return
. Przykład poniżej, implementuje metodę GetMonthsForQuarter, której zadaniem jest zwrócenie nazw miesięcy dla przekazanego kwartału z zastosowaniem iteratora. Do zwrócenia nazwy miesięcy dla kwartału korzystamy z tablicy MonthNames klasy DateTimeFormatInfo udostępnionej przez CultureInfo z przestrzeni nazw System.Globalization.
static void Main(string[] args)
{
foreach (var month in GetMonthsForQuarter(2))
Console.WriteLine(month);
}
public static IEnumerable<string> GetMonthsForQuarter(int quarter)
{
string[] monthNames = CultureInfo.CurrentCulture.DateTimeFormat.MonthNames;
for(int i = (quarter - 1) * 3; i < quarter * 3; i++)
yield return monthNames[i];
}
Metoda GetMonthsForQuarter zwraca typ IEnumerable<string>
, jednak kod zawierający instrukcję yield return
zwraca pojedynczy element tablicy. Cechą iteratorów jest generowanie kolejnych wartości, jedna po drugiej używając instrukcji yield return
. W odróżnieniu od zwykłej metody, która kończy działanie po słowie return
, iterator działa tak długo, aż pętla nie dojdzie do końca lub użyta zostanie instrukcja yield break
, lub zgłoszony zostanie wyjątek.
Kod iteratora poniżej jest stosunkowo prosty, lecz zrozumienie jego działania wymaga nieco więcej uwagi.
public static IEnumerable<int> GetNumbers()
{
yield return 2011;
yield return 8;
yield return 16;
}
Mogłoby się wydawać, że język C# przygotowując wynik dla iteratora tworzy zmienną przechowującą komplet danych. Nie jest to jednak prawda. Zachęcam do stworzenia pętli z warunkiem true
, która to będzie zwracać kolejne wartości poprzez yield return
. W razie problemów zamieszczam kod realizujący opisane działanie.
using System;
using System.Collections.Generic;
namespace Hello
{
class Program
{
static void Main(string[] args)
{
foreach (var num in GetNumbers())
Console.WriteLine(num);
}
private static readonly Random r = new Random();
private static IEnumerable<int> GetNumbers()
{
while (true)
yield return r.Next(0, 1000);
}
}
}
Wróćmy do metody GetNumbers. Spójrzmy na jej bliźniaczą wersję, zwracającą typ List<int>. Następnie korzystając z narzędzia ILDASM umożliwiającego deasemblację kodu (aplikacja znajduje się w .NET SDK). Porównajmy wygenerowaną strukturę, zaczynając od niżej zamieszczonej metody GetNumbers.
public IEnumerable<List<int>> GetNumbers()
{
return new List<int> { 2015, 2, 10 };
}
Analizując domyślne okno możemy odczytać takie informacje jak:
private
),Poniżej zamieszczam strukturę metody GetNumbers, w której widoczne jest użycie konstruktora .ctor() dla typu List<int>
, oraz trzykrotne wykonanie metody Add. Jest to standardowe rozwinięcie konstruktora przyjmującego kolekcję wartości.
Wróćmy do metody GetNumbers korzystającej z yield return
i ponownie przeanalizujmy informacje zwracane przez narzędzie ILDASM.
public IEnumerable<int> GetNumbers()
{
yield return 2011;
yield return 8;
yield return 16;
}
To, co wykonywane jest pod spodem, w przypadku zastosowania iteratora możemy określić jako "skomplikowane". Oprócz już wymienionych elementów widoczna jest implementacja interfejsów IEnumerable<int>
oraz IEnumerator<int>
, które tworzą klasę zagnieżdżoną. Natomiast kod metody GetNumbers został umieszczony wewnątrz MoveNext. Kompilator zastosował podział umożliwiający powrót we właściwe miejsce po każdorazowym zwróceniu wartości poprzez zastosowanie instrukcji yield return
.
Korzystając z iteratorów należy pamiętać, że faktyczne wykonanie kodu iteratora następuje wraz z rozpoczęciem iteracji. Przez co w sytuacji, gdy decydujemy się na sprawdzenie poprawności przekazanych argumentów, ich rzeczywista weryfikacja zostanie odroczona do wykonania iteracji.
Troska Robert