Enumeratory #
Enumerator to podobny koncept do iteratorów z C++. Enumerator to kursor działający w trybie “tylko do odczytu”, który pozwala na jednokierunkowe przeglądanie sekwencji wartości.
Żeby obiekt można było nazwać enumeratorem to musi posiadać publiczną bezparametrową metodę bool MoveNext()
i właściwość Current
. Najczęściej implementuje się interfejs System.Collections.Generics.IEnumerator<T>
(lub rzadziej System.Collections.IEnumerator
), który dostarcza wspomnianą metodę i właściwość.
Definicja tych interfejsów wygląda następująco:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
T Current { get; }
}
Często metody Reset
się nie implementuje, to jest rzuca ona wyjątek NotSupportedException
.
Typ enumerowalny #
Typ enumerowalny to z kolei obiekt, który jest w stanie stworzyć nowy enumerator. Musi mieć publiczną metodę GetEnumerator()
, która zwraca enumerator (może to być metoda rozszerzająca). I tu znów najczęściej implementuje się systemowy interfejs System.Collections.Generic.IEnumerable<T>
(lub rzadziej System.Collections.IEnumerable
).
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
Pętla foreach
#
Wszystkie systemowe kolekcje, tablice i stringi są enumerowalne. Wszystko co jest enumerowalne może być użyte wewnątrz pętli foreach
:
List<int> collection = [1, 2, 3, 4, 5, 6, 7, 8];
foreach(int item in collection)
{
Console.WriteLine(item);
}
Pętla foreach
jest cukierkiem składniowym, kompilator przekształca to wyrażenie następująco:
IEnumerator<int> enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item);
}
}
finally
{
enumerator?.Dispose();
}
Metoda iterująca #
Korutyna to specjalny rodzaj funkcji, która ma zdolność do wstrzymania swojego działania w dowolnym momencie i zwrócenia kontroli do kodu, który ją wywołał, a następnie, po jakimś czasie, wznowienia działania dokładnie od miejsca, w którym została wstrzymana.
Metody iterujące to jeden z dwóch przykładów korutyn w C#. Taka metoda musi zwracać IEnumerable<T>
lub IEnumerator<T>
(lub IEnumerable
lub IEnumerator
) i używać instrukcji yield
. Taka metoda jest przez kompilator traktowana w szczególny sposób. Kompilator w jej miejsce tworzy maszynę stanów, która zapamiętuje stan iteracji, czyli lokalne zmienne oraz informację o miejscu wstrzymania.
public static IEnumerable<int> Fibonacci(int count)
{
for (int i = 0, prev = 1, curr = 1; i < count; i++)
{
yield return prev;
(prev, curr) = (curr, prev + curr);
}
}
Taka maszyna stanów wygląda następująco dla tej metody:
[CompilerGenerated]
private sealed class <Fibs>d__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
private int <>1__state;
private int <>2__current;
private int <>l__initialThreadId;
private int count;
public int <>3__count;
private int <i>5__1;
private int <prev>5__2;
private int <curr>5__3;
int IEnumerator<int>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
[return: Nullable(0)]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <Fibs>d__1(int <>1__state)
{
this.<>1__state = <>1__state;
<>l__initialThreadId = Environment.CurrentManagedThreadId;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
}
private bool MoveNext()
{
int num = <>1__state;
if (num != 0)
{
if (num != 1)
{
return false;
}
<>1__state = -1;
int num2 = <curr>5__3;
int num3 = (<curr>5__3 = <prev>5__2 + <curr>5__3);
<prev>5__2 = num2;
<i>5__1++;
}
else
{
<>1__state = -1;
<i>5__1 = 0;
<prev>5__2 = 1;
<curr>5__3 = 1;
}
if (<i>5__1 < count)
{
<>2__current = <prev>5__2;
<>1__state = 1;
return true;
}
return false;
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
[DebuggerHidden]
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
<Fibs>d__1 <Fibs>d__;
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
<Fibs>d__ = this;
}
else
{
<Fibs>d__ = new <Fibs>d__1(0);
}
<Fibs>d__.count = <>3__count;
return <Fibs>d__;
}
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<int>)this).GetEnumerator();
}
}
Samo wywołanie metody iterującej powoduje tylko stworzenie maszyny stanów. Sam kod metody iterującej jest wywoływany po wywołaniu MoveNext
na enumeratorze od ostatniego stanu. Obliczana jest następna wartość, instrukcja yield return
zwraca obliczoną wartość przez zapis jej do właściwości Current
i kontrola wraca do wywołującego.
Istnieje także instrukcja yield break
, która również zwraca kontrolę do wywołującego, ale sprawia że MoveNext
zwraca false
, oznaczając brak dalszych elementów.
public static IEnumerable<int> Fibonacci(int count)
{
for (int i = 0, prev = 1, curr = 1; i < count; i++)
{
if (i > 45) yield break;
yield return prev;
(prev, curr) = (curr, prev + curr);
}
}
Kompozycja metod iterujących #
Metody iterujące można ze sobą składać. Załóżmy, że mamy dwie metody:
public static IEnumerable<int> Fibonacci(int n)
{
int current = 0, next = 1;
for (int i = 0; i < n; i++)
{
yield return current;
(current, next) = (next, current + next);
}
}
public static IEnumerable<int> Odds(IEnumerable<int> sequence)
{
foreach (var item in sequence)
{
if (item % 2 == 1) yield return item;
}
}
Można je ze sobą składać w następujący sposób:
var sequence = Odds(Fibonacci(10));
Console.WriteLine("Odd elements of the fibonacci sequence:");
foreach (var item in sequence)
{
Console.WriteLine(item);
}
Przepływ kontroli przedstawia diagram sekwencji:
sequenceDiagram participant Main participant Odds participant Fibonacci activate Main Main->>+Odds: MoveNext() deactivate Main activate Odds Odds->>Fibonacci: MoveNext() deactivate Odds activate Fibonacci Fibonacci->>Odds: 1 deactivate Fibonacci activate Odds Odds->>Main: 1 deactivate Odds activate Main Main->>Odds: MoveNext() deactivate Main activate Odds Odds->>Fibonacci: MoveNext() deactivate Odds activate Fibonacci Fibonacci->>Odds: 1 deactivate Fibonacci activate Odds Odds->>Main: 1 deactivate Odds activate Main Main->>Odds: MoveNext() deactivate Main activate Odds Odds->>Fibonacci: MoveNext() deactivate Odds activate Fibonacci Fibonacci->>Odds: 2 deactivate Fibonacci activate Odds Odds->>Fibonacci: MoveNext() deactivate Odds activate Fibonacci Fibonacci->>Odds: 3 deactivate Fibonacci activate Odds Odds->>Main: 3 deactivate Odds activate Main Main->>Odds: ... deactivate Main
Metoda Odds
jest filtrem dla sekwencji Fibonacci
, na tej zasadzie opieraja się większość metod z LINQ.
Kod źródłowy - IteratorMethods
Implementacja własnego typu enumerowalnego #
Żeby nasz typ własny typ był enumerowalny, najprościej jest zaimplementować interfejs IEnumerable
. Najczęściej metodę GetEnumerator
implementuje się jako iterującą z wykorzystaniem instrukcji yield
.
public class Stack<T> : IEnumerable<T>
{
private T[] _items = new T[8];
public int Count { get; private set; }
public void Push(T item)
{
if (_items.Length == Count)
{
Array.Resize(ref _items, _items.Length * 2);
}
_items[Count++] = item;
}
public T Pop()
{
if (Count == 0)
{
throw new InvalidOperationException("Stack is empty");
}
return _items[--Count];
}
public IEnumerator<T> GetEnumerator()
{
int count = Count;
while (count-- > 0)
{
yield return _items[count];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Taki enumerowalny stos z powodzeniem można użyć w instrukcji foreach
:
Stack<string> stack = new Stack<string>();
stack.Push("The");
stack.Push("quick");
stack.Push("brown");
stack.Push("fox");
stack.Push("jumps");
stack.Push("over");
stack.Push("the");
stack.Push("lazy");
stack.Push("dog");
foreach (var str in stack)
{
Console.WriteLine(str);
}
Kod źródłowy - EnumerableStack