20/11/2008

Tajemnica yield

Home

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:

Unknown said...

bardzo fajnie opisane, ale mimo wszystko brakło komentarzy przy kazdej linii kodu, co by trochę rozjaśniło zwykłem laikom ;)

Michał Komorowski said...

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.

Kamil Wysocki said...

świetnie opisane. Dzięki.

Unknown said...

masz złą nazwe konstruktora na poczatku

Michał Komorowski said...

Dziękuję za zwrócenie uwagi, już poprawiłem.

Anonymous said...

W pierwszym listingu dziedziczysz po interfejsie IEnumerable (implementujesz go "jawnie"). Wydaje mi się, że nie jest to konieczne?

Michał Komorowski said...

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