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);
}
}
}