yield

Jak pokazałem w poprzednim wpisie proces tworzenia kolekcji wyliczeniowej jest złożony. W C# istnieją jednak iteratory, które potrafią zautomatyzować ten proces.

Iterator jest to blok kodu, który uzyskuje (yield) w odpowiedniej kolejności wartości. Dodatkowo iterator nie jest częścią klasy wyliczeniowej. Określa on tylko sekwencje, w której enumerator powinien zawracać wartości. Czyli iterator jest tylko opisem sekwencji wyliczania, dzięki któremu kompilator w C# może stworzyć swój własny enumerator.

W samym opisie tego zagadnienia nie jest na pewno wystarczający. Musiałem tutaj użyć trzech słów angielskich jak:enumerator i iterator i yield, ponieważ słownik polski nie ma programistycznych odpowiedników tych słów, czyniąc ten opis mniej czytelnym.

Czas więc na przykład.

Prosty iterator

Oto przykład prostej kolekcji, która korzysta z działanie iteratora (yeild) Klasa używa klasy generycznej List od T do przetrzymywanie informacji i korzysta z metody FillList() aby wypełnić tę listę danymi.
Kolekcja ta implementuje też interfejs IEnumerable od T . Implementacja metody GetEnumerator implementuje rozwiązanie przy użyciu iteratora, czyli słowa kluczowego yield.

class SimpleCollection<T> : IEnumerable<T>
{
    private List<T> data = new List<T>();

    public void FillList(params T [] items)
    {
        foreach (var item in items)
        {
            data.Add(item);
        }
    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        foreach (var datum in data)
        {
            yield return datum;
        }
    }

    System.Collections.IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

Metoda GetEnumerator tym razem wydaje się dosyć prosta, ale wymaga głębszego podglądu. Jak widzisz metoda ta nie zwraca typu, który by implementował IEnumerator od T. Zamiast tego jest pętla, która przechodzi po liście danych i zwraca tylko jeden element w turze.

Kluczem do zrozumienie tego, co tutaj się dzieje jest słowo kluczowe yeild.

Wyrażenie yield zatrzymuje tymczasowe wykonanie metody i wysyła jedną wartość do wywoływacza. Kiedy następuje kolejne wywołanie metoda ta rusza dalej ,aż do kolejnego zatrzymania, w którym znowu jest wysłana jedna wartość do wywoływacza. W pewnym momencie pętla nie pobiera już więcej danych, kończąc też w ten sposób działanie metody GetEnumerator. Iteracja dobiegła końca.

Pamiętaj GetEnumerator nie jest zwykłą metodą. Kod wewnątrz metody GetEnumerator jest traktowany jako iterator. Kompilator za ciebie generuje implementacje interfejsu IEnumerator od T. Klasa stworzona przez kompilator zawiera więc właściwość Current i metodę MoveNext().

Ta implementacja jest dokładnie dopasowana do funkcjonalności metody GetEnumerator od T. Nigdy nie zobaczysz tego wygenerowanego kodu no, chyba że pogrzebiesz w kodzie IL. ,ale nie jest to takie istotne jak się wydaje.

Słowo kluczowe yield redukuje pisanie kodu. Oto jak działa kolekcja SimpleCollection w pętli foreach.

SimpleCollection<int> sc = new SimpleCollection<int>();

sc.FillList(9, 8, 7, 6, 5, 4, 3, 2, 1);

foreach (var number in sc)
{
    Console.Write(" {0} ,",number);
}

Kod wyświetla zawartość tej kolekcji:

wynik simple collection

Jeżeli chcesz wprowadzić alternatywną iteracje to nie ma problemu. Wystarczy zadeklarować właściwość, która implementuje interfejs IEnumerable i użyć iteratora do zwracania danych.

public IEnumerable<T> Reverse
{
    get 
    {
        for(int i = data.Count - 1;i >=0; i--)
            yield return data[i];
    }
}

Wywołanie właściwości Reverse wygląda tak:

SimpleCollection<int> sc = new SimpleCollection<int>();

sc.FillList(9, 8, 7, 6, 5, 4, 3, 2, 1);

foreach (var number in sc.Reverse)
{
    Console.Write(" {0} ,",number);
}


image

Jak widzisz słowo kluczowe yield jest bardzo użyteczne. Mogę napisać np. prostą metodę, która zwraca IEnumerable od T ,ale pomija kilka pierwszych wartości. Metoda ta przypomina metodę LINQ Skip(). W sumie używając słowa yeild możesz sam napisać takie proste metody.

public IEnumerable<T> MySkip(int whilethis)
{
    for (int i = whilethis; i < data.Count; i++)
    {
        yield return data[i];
    }
}

Użycie metody MySkip.

SimpleCollection<int> sc = new SimpleCollection<int>();
sc.FillList(9, 8, 7, 6, 5, 4, 3, 2, 1);

foreach (var number in sc.MySkip(7))
{
    Console.Write(" {0} ,",number);
}


image

Jeszcze prostszy przykład użycia słowa yield.

Tak jak napisałem wcześniej słowo kluczowe yeild zatrzymuje działanie metody i co jedną turę w iteracji przesyła tylko jedną wartość.

Najlepiej udowodnić to tym przykładem konsolowej aplikacji.

static public IEnumerable<string> Napisy()
{
    yield return "Ala";
    yield return "ma";
    yield return "radioaktywnego";
    yield return "kota";
    yield return "z";
    yield return "Marsa";
}

static void Main(string[] args)
{
    int i = 0;
    foreach (var item in Napisy())
    {
        i++;
        Console.WriteLine(item);
    }
}

Używając breakpointów można zobaczyć , że metoda w każdym cyklu pętli foreach przesyła tylko jedną wartość i wie, które wartości już wystąpiły . Magia kodu po kompilacji, którego nie widzimy na to nie pozwala.

image

Ponowne definiowanie Enumeratora dla drzewa binarnego

Teraz gdy wiem ,że istnieje lepszy sposób na implementacje interfejsu IEnumerator, to dlaczego z niego nie skorzystać.

Komentując implementacje interfejsu IEnumerator, którą napisałem poprzednio napiszę ją jeszcze raz ale tym razem ze słowem yield. Wygląda to tak.

IEnumerator<TItem> IEnumerable<TItem>.GetEnumerator()
{
    if (this.LeftTree != null)
    {
        foreach (TItem item in this.LeftTree)
        {
            yield return item;
        }
    }

    yield return this.NodeData;

    if (this.RightTree != null)
    {
        foreach (var item in this.RightTree)
        {
            yield return item;
        }
    }
}

Kod ten działa w stylu tego samego rekursywnego algorytmu, który wcześniej był użyty do wyświetlania zawartości drzewa.

Jeżeli lewe drzewo nie jest puste, to pierwsza pętla foreach wywoła kolejny raz metodę GetEnumerator dla lewego drzewa. Ten proces będzie trwać , aż do momentu przesłania wszystkich wartości po stronie lewego drzewa do wywoływacza głównego. Później główny węzeł jest przesłany ,a potem w podobny sposób wartości węzłów prawego poddrzewa są przesyłane do wywoływacza głównego.

W sumie nie ma tutaj niczego skomplikowanego , ale od wyobrażenia sobie rekursji działania tej metody może boleć głowa.

Pozostało przetestować tę implementacje tym samym kodem, co w poprzednim wpisie.

Tree<double> tree4 = new Tree<double>(double.NaN);
tree4.Insert(3.14);
tree4.Insert(5.555);
tree4.Insert(0.1);
tree4.Insert(-12.12);
tree4.Insert(15.15);
tree4.Insert(0);
tree4.Insert(double.PositiveInfinity);
tree4.Insert(double.NegativeInfinity);
tree4.Insert(double.MaxValue);
tree4.Insert(double.MinValue);
tree4.Insert(-8.16);

foreach (double item in tree4)
{
    Console.WriteLine(" {0} ", item);
}
Console.ReadKey();

Pętla foreach w konsoli wyświetla zawartość drzewa binarnego tak samo, jak wcześniej.

image

Na tym kończy się ten dział, w którym omówiłem klasę generyczną, metodę generyczną , interfejs generyczny, jak i implementacje interfejsów IEnumerable od T i IEnumerator od T.