Czy zastanawialiście się kiedyś jak działa słowo kluczowe yield? Jeśli ktoś nie kojarzy tej konstrukcji to w telegraficznym skrócie pozwala ona (między innymi) w bardzo łatwy sposób zaimplementować interfejs IEnumerable. Interfejs ten wymagana dostarczenia tylko jednej metody, która powinna zwrócić instancję klasy implementującej IEnumerator. Zaimplementowanie tego interfejsu nie powinno przysporzyć znacznych trudności ale wymaga już trochę większego nakładu pracy. Przykładowe, uproszczone użycie yield mogłoby wyglądać tak:
public class Counter : IEnumerable { private int i = 0; public Counter(int i) { this.i = i; } public IEnumerator GetEnumerator() { while(i>0) yield return i--; } }
Tylko tyle i aż tyle. Nie musimy pisać kodu dla metod MoveNext, Reset czy też właściwości Current wymaganych przez IEnumerator. Zamiast tego otrzymujemy kilkulinijkowy elegancki kod. Możemy oczywiście napisać teraz coś w tym rodzaju:
foreach (int i in new Counter(10)) Console.WriteLine(i);
Zanim zaczniecie czytać dalej zastanówcie się teraz przez chwilę w jaki sposób to działa. Teraz możemy przejść do clue tego posta. Otóż okazuje się, że słowo kluczowe yield to nic innego jak lukier syntaktyczny. Nie kryje się zanim żadna magia. Po prostu kompilator po napotkaniu yield generuje dynamicznie kod enumeratora. Możemy to bardzo łatwo zobaczyć korzystając z reflektora Lutz Roeder’s Reflector. To co zobaczymy będzie koncepcyjnie podobne do kodu poniżej.
public class Counter { ... public IEnumerator GetEnumerator() { //Utworzenie enumeratora InnerEnumerator ie = new InnerEnumerator(0); //Ustawienie wskazania na obiekt, po którym będziemy enumerować ie.current = this; reutrn ie; } private sealed class InnerEnumerator : IEnumerator { //Stan w jakim znajduje się enumerator //0 - stan początkowy //1 - stan pośredni //-1 - stan końcowy private int state; //Ostatnia wartość zwrócona przez enumerator private int current; //Obiekt, po którym będziemy enumerować public Counter counter; public InnerEnumerator(int state) { //Ustawienie stanu inicjalnego this.state = state; } public bool MoveNext() { switch (this.state) { case 0: //Jeśli warunek początkowy rozpoczęcia działania enumeratora //nie będzie spełniony to przechodzimy do stanu końcowego this.state = -1; //Jeśli są jeszcze jakieś wartości do odwiedzenia przez enumerator while (this.counter.i > 0) { //Wyznacz kolejną wartość this.current = this.counter.i--; //Być może są jeszcze jakieś wartości do odwiedzenia //dlatego ustawiamy stan pośredni this.state = 1; return true; LABEL: //Jeśli nie będzie już wartości do odwiedzenia to //należy zakończyć pracę enumeratora this.state = -1; } break; case 1: //Kontynuujemy pracę enumeratora goto LABEL; } //Enumerator odwiedził wszystkie elementy return false; } public object Current { get{ return this.current; } } ... } ... }
Z kodu usunąłem niepotrzebne w tym kontekście fragmenty i zmieniłem go, żeby był prostszy w zrozumieniu. Wygenerowany kod jest prawidłowy jako MSIL ale jako C# nie skomiluje się ze względu niedozwolone użycie instrukcji goto. Całość jest chyba łatwa do zrozumienia, a najistotniejsza jest metoda MoveNext(), w której tak naprawdę możemy zobaczyć to co napisaliśmy w GetEnumerator. Każda iteracja pętli powoduje przejście do następnego elementu. Polecenia skoku użyto aby przy kolejnych wywołaniach MoveNext wskoczyć do środka pętli i kontynuować jej wykonanie. Proste, nie :)
7 comments:
bardzo fajnie opisane, ale mimo wszystko brakło komentarzy przy kazdej linii kodu, co by trochę rozjaśniło zwykłem laikom ;)
Dodałem komentarze, które opisują chyba każde miejsce w kodzie, które może budzić jakieś wątpliwości. Mam nadzieję, że okażą się pomocne.
świetnie opisane. Dzięki.
masz złą nazwe konstruktora na poczatku
Dziękuję za zwrócenie uwagi, już poprawiłem.
W pierwszym listingu dziedziczysz po interfejsie IEnumerable (implementujesz go "jawnie"). Wydaje mi się, że nie jest to konieczne?
Z czysto technicznego punktu widzenia nie muszę używać IEnumerable i pętla foreach poradzi sobie z taką sytuację. Sądzę jednak, że to zła praktyka. Standardowe interfejsy to taki wspólny, rozumiany przez wszystkich język. Skoro można po czymś enumerować to czemu się z tym kryć. Po drugie dzięki dziedziczeniu po IEnumerable z daną klasą można pracować w taki sposób jak z innymi klasami implementującymi ten interfejs np.: przypisać je do tej samej zmiennej typu IEnumerable .
Post a Comment