Paweł Łukasiewicz
2015-07-28
Paweł Łukasiewicz
2015-07-28
Udostępnij Udostępnij Kontakt

Słowo kluczowe yield pozwala na przygotowanie niestandardowych iteracji dokonywanych na kolekcjach. Możemy wyróżnić dwa scenariusze w których użycie słowa kluczowego yield może okazać się przydatne:

  • niestandardowe operacje przeprowadzane na kolekcji bez potrzeby tworzenia kolekcji tymczasowej;
  • przechowywanie informacji o tym co się zdarzyło bądź zmieniło od czasu uruchomienia aplikacji, a ściślej, w trakcie wykonywanie operacji na naszej kolekcji (stateful iteration).


Niestandardowe operacje

Niestandardowe operacje najłatwiej będzie zrozumieć na gotowym przykładzie.
Załóżmy, że mamy prostą listę o nazwie MyList, która składa się z 5 liczb całkowitych: 1, 2, 3, 4, 5. Lista te jest przetwarzana z poziomu głównej metody aplikacji konsolowej, tj. Main().

Nasza metoda Main() będzie przetwarzała elementy listy a następnie wartość danego elementu wypisze w konsoli:

using System;
using System.Collections.Generic;
namespace ListForeach
{
    class Program
    {
        static List<int> MyList = new List<int>();
        static void Main(string[] args)
        {
            FillList();
            foreach (int item in MyList)
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
            // Wynik działania programu
            // 1
            // 2
            // 3
            // 4
            // 5
        }
        static void FillList()
        {
            MyList.Add(1);
            MyList.Add(2);
            MyList.Add(3);
            MyList.Add(4);
            MyList.Add(5);
        }
    }
}
Skomplikujmy teraz nieco sytuację. Załóżmy, że chcemy w danej kolekcji posiadać tylko liczby większe od 3. Oczywistym jest, że programista doda nową metodę, która wykona sprawdzenie, a wszystkie wartości będzie przechowywał na liście tymczasowej. Następnie dane te zostaną zwrócone i będzie można na nich operować:
static IEnumerable<int> FilterWithoutYield()
{
    List<int> temp = new List<int>();
    foreach (var item in MyList)
    {
        if (item > 3)
            temp.Add(item);
    }
    return temp;
}
Powyższe podejście jest poprawne. Najlepiej byłoby jednak pozbyć się kolekcji tymczasowej. W tym momencie pojawia się słowo kluczowe yield. Poniżej prosty przykład użycia tego słowa kluczowego.
Yield pozwala na powrót do metody wywołującej, metoda wywołująca wykonuje swoje zadanie a następnie następuje powrót do kolekcji w miejscu w którym ta kolekcja została opuszczona. Innymi słowy, yield przenosi kontrolę programu pomiędzy metodą wywołującą a kolekcją.
static IEnumerable<int> FilterWithYield()
{
    foreach (var item in MyList)
    {
        if (item > 3) yield return item;
    }
}

Omówmy dla przykładu dokładny przepływ kontroli:

  • Krok 1 - caller (metoda wywołująca – w naszym przypadku Main()) wywołuje metodę do przetwarzania danych, tj. poszukiwanie wartości większych od 3;
  • Krok 2 – wewnątrz metody wykonywana jest pętla, która przechodzi po kolejnych liczbach, aż znajdzie liczbę większą niż 3. Jak tylko liczba taka zostanie wykryta słowo kluczowe yield przekazuje te dane z powrotem do metody wywołującej;
  • Krok 3 - caller wyświetla wartość tej liczby w konsoli a następnie wchodzi z powrotem do metody w celu poszukiwania kolejnych liczb. Tym razem nie zaczyna wykonywania metody od nowa a dokładnie od miejsca w którym skończył ostatnio. Pamięta stan i zaczyna sprawdzenie od kolejnej liczby, tj. 5. Iteracja dalej odbywa się jak w powyższym punkcie.
Poniżej kompletny przykład wraz z opisem wykonania:
using System;
using System.Collections.Generic;
namespace UsingYield
{
    class Program
    {
        static List<int> MyList = new List<int>();
        static void Main(string[] args)
        {
            // wypełniamy naszą listę danymi
            FillMyList();
            // wywołujemy pętle foreach na naszej metodzie, która zwraca jedynie liczby większe niż 3
            foreach (var item in ListWithYieldReturn())
            {
                // wyświetlamy wartości, które spełniają warunek przez nas zdefiniowany
                Console.WriteLine(item);
            }
            Console.ReadKey();
            // Wynik działania programu
            // 4
            // 5
        }
        static void FillMyList()
        {
            MyList.Add(1);
            MyList.Add(2);
            MyList.Add(3);
            MyList.Add(4);
            MyList.Add(5);
        }
        static IEnumerable<int> ListWithYieldReturn()
        {
            // Przechodzimy po elementach naszej listy
            foreach (var item in MyList)
            {
                // kontrolka do caller'a (metody wywołującej) jest przekazywana jeżeli warunek jest spełniony
                if (item > 3)
                    yield return item;
            }
        }
    }
}


Stateful interation

Kolejny przykład musimy nieco bardziej skomplikować. Załóżmy, że chcemy wyświetlić łączną wartość liczb w naszej kolekcji.

Będziemy przeglądać elementy naszej listy od 1 do 5 a w trakcie przeglądania tych wartości będziemy je do siebie dodawać. Zaczynamy od wartości 1, która jako wynik zwraca 1. Następnie przechodzimy do wartości 2 a ogólny wynik w tym przypadku to 3, itd.

Innymi słowy, chcemy przechodzić przez elementy kolekcji oraz zarządzać stanem całkowitym i zwracać tą wartość do naszej metody wywołującej. Zmienna runningTotal będzie cały czas przechowywała starą wartość za każdym razem gdy kontrola zostanie z powrotem przekazana do metody obliczającej sumę:

using System;
using System.Collections.Generic;
namespace YieldState
{
    class Program
    {
        static List<int> MyList = new List<int>();
        static void Main(string[] args)
        {
            FillMyList();
            foreach (var item in RunningTotal())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
            // Wynik działania programu
            // 1
            // 3
            // 6
            // 10
            // 15
        }
        static IEnumerable<int> RunningTotal()
        {
            int runningTotal = 0;
            foreach (var item in MyList)
            {
                runningTotal += item;
                yield return runningTotal;
            }
        }
        static void FillMyList()
        {
            MyList.Add(1);
            MyList.Add(2);
            MyList.Add(3);
            MyList.Add(4);
            MyList.Add(5);
        }
    }
}