Enumeratory w .NET 2.0

Proste polecenie "wyliczenia" elementów danej kolekcji i np. wykonanie pewnych operacji na każdym z elementów z punktu widzenia aplikacji i języka nie jest wcale zadaniem trywialnym - wymaga chociażby synchronizacji w wypadku systemów wielowątkowych. W .NET 2.0 pojawiły się dodatkowe słowa kluczowe ułatwiające implementację "wyliczeń".

Proste polecenie "wyliczenia" elementów danej kolekcji i np. wykonanie pewnych operacji na każdym z elementów z punktu widzenia aplikacji i języka nie jest wcale zadaniem trywialnym - wymaga chociażby synchronizacji w wypadku systemów wielowątkowych. W .NET 2.0 pojawiły się dodatkowe słowa kluczowe ułatwiające implementację "wyliczeń".

Często w aplikacji trzeba móc "przejść" po pewnej kolekcji i wykonać pewną operację dla każdego elementu składowego. W .NET służy do tego pętla foreach i tzw. enumeratory, czyli specjalny interfejs, który ze strony kolekcji zapewnia odpowiednie mechanizmy pozwalające działać w pętli. Interfejs zawiera trzy metody: Current (zwraca wartość aktualną), MoveNext (przesuwa do następnego elementu), Reset (przesuwa na początek kolekcji). Właściwie kolekcja "zwraca" enumerator, który jest pobierany przez instrukcję foreach i służy potem do iteracji.

W .NET 1.1 programista, chcąc zapewnić możliwość iteracji po kolekcji, musiał implementować te trzy metody, co w wypadku bardziej złożonych struktur - jak chociażby drzewa - było dosyć karkołomnym zadaniem.

W C# 2.0 wprowadzono mechanizm, który w znacznym stopniu upraszcza implementację tego interfejsu. W języku pojawiło się nowe słowo kluczowe yield. Zadaniem tej instrukcji jest generowanie kolejnych wartości, które mają być zwracane przez enumerator.

yield return <wartość> zwraca określoną wartość, a yield break oznacza, że zakończyliśmy generowanie sekwencji zwracanej przez iterator.

Załóżmy, że w programie znajduje się funkcja, która zwraca enumerator typów string. Można, oczywiście, wyniku działania takiej funkcji użyć jako argumentu pętli:

1 IEnumerable<string> Lista() {

2 yield return "jeden";

3 yield return "dwa";

4 yield return "trzy";

5 }

...

6 foreach (string str in Lista()) {

7 Console.WriteLine(str);

8 }

Istotna jest kolejność działania takiej aplikacji. Właściwie instrukcja yield "generuje" funkcje Current, MoveNext i Reset, tyle że kompilator robi to automatycznie za programistę. W tym wypadku podczas pierwszego pobrania wartości zostanie wykonana instrukcja w wierszu 6, co spowoduje skok do wiersza 1 i zwrócenie wartości z wiersza 2. Następnie zostanie, oczywiście, wykonana instrukcja 7 (czyli zwrócona wartość będzie wyprowadzona na konsoli). W kolejnym kroku foreach ma pobrać kolejną wartość z kolekcji, a więc ponownie odwołuje się do funkcji Lista(), ale od razu skacze do wiersza 3. yield break służy do zasygnalizowania, że to koniec generowania sekwencji - właściwie może być używany podobnie jak break. Poniżej pokazana jest funkcja generująca ciąg Fibonacciego, używająca mechanizmów yield:

IEnumerable<long> fib() {

yield return 0;

long i = 0, j = 1;

while (true) {

yield return j;

long temp = i;

i = j; j = temp + j;

if (i > 1000) yield break;

}

}

...

foreach (long l in fib()) {

Console.WriteLine(l);

}

Proszę zobaczyć, że w tym wypadku logika działania jest bardzo podobna do zwykłego algorytmu generowania tego ciągu - tyle że co pewien czas instrukcją yield zwracamy wynik do enumeratora. Instrukcja yield break pozwala przerwać generowanie po przekroczeniu przez i wartości 1000 - proszę zwrócić uwagę, że bez tego warunku pętla generowania liczb byłaby nieskończona.

W wypadku przykładowej kolekcji z artykułu "Generics - typy ogólne"

class MyArrayEnum<T>:IEnumerable<T> {

int pos = 0;

T[] arr = new T[50];

public T First ...

public int Add(T val)...

public IEnumerator<T> GetEnumerator() {

foreach (T elem in arr) {

yield return elem;

}

}

IEnumerator IEnumerable.GetEnumerator() {

throw new System.Exception("Method or operation not implemented.");

}

}

enumerator może się odwołać do enumeratora tablicy i zwracać odpowiednie elementy.

W .NET 1.1 enumerator miał trochę inną sygnaturę - operował na wartościach typu object. W .NET 2.0 można, oczywiście, implementować taki interfejs, ale lepiej używać typów ogólnych, które od razu zapewniają, że to, co jest zwracane w wyliczeniu, ma konkretny typ. W tym wypadku dodatkowo zaimplementowany został "ogólny" enumerator, który spowoduje wyjątek, gdy aplikacja spróbuje go użyć.


Zobacz również