środa, 08 marca 2017

Iterator

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:

  • struktura przestrzeni nazw oraz klasy,
  • modyfikator klasy Program (private),
  • konstruktor klasy Program (.ctor),
  • parametry oraz zwracany typ metody Main, oraz GetNumbers.

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