Słowo yieldkluczowe umożliwia utworzenie IEnumerable<T>formularza w bloku iteratora . Ten blok iteratora obsługuje odraczanie wykonywania, a jeśli nie znasz tej koncepcji, może wydawać się niemal magiczny. Jednak na koniec dnia to tylko kod, który wykonuje się bez dziwnych sztuczek.
Blok iteratora można opisać jako cukier syntaktyczny, w którym kompilator generuje maszynę stanu, która śledzi, jak daleko posunęło się wyliczenie liczby. Aby wyliczyć wyliczenie, często używasz foreachpętli. Jednakże foreachpętli jest również cukier składniowym. Jesteś więc dwiema abstrakcjami usuniętymi z prawdziwego kodu, dlatego początkowo może być trudno zrozumieć, jak to wszystko działa razem.
Załóżmy, że masz bardzo prosty blok iteratora:
IEnumerable<int> IteratorBlock()
{
Console.WriteLine("Begin");
yield return 1;
Console.WriteLine("After 1");
yield return 2;
Console.WriteLine("After 2");
yield return 42;
Console.WriteLine("End");
}
Rzeczywiste bloki iteratora często mają warunki i pętle, ale kiedy sprawdzasz warunki i rozwijasz pętle, nadal kończą się jako yieldinstrukcje przeplatane z innym kodem.
Do wyliczenia bloku iteratora foreachużywana jest pętla:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Oto wynik (tutaj nie ma niespodzianek):
Zaczynać
1
Po 1
2)
Po 2
42
Koniec
Jak wspomniano powyżej, foreachcukier syntaktyczny:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
Próbując rozwiązać ten problem, stworzyłem schemat sekwencji z usuniętymi abstrakcjami:

Automat stanów wygenerowany przez kompilator również implementuje moduł wyliczający, ale dla uproszczenia diagramu pokazałem je jako osobne instancje. (Gdy automat stanowy jest wyliczany z innego wątku, faktycznie otrzymujesz osobne instancje, ale ten szczegół nie jest tutaj ważny).
Za każdym razem, gdy wywołujesz blok iteratora, tworzona jest nowa instancja automatu stanów. Jednak żaden kod w bloku iteratora nie jest wykonywany, dopóki nie zostanie wykonany enumerator.MoveNext()po raz pierwszy. Tak działa odroczone wykonywanie. Oto (raczej głupi) przykład:
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
W tym momencie iterator nie wykonał się. WhereKlauzula tworzy nowy IEnumerable<T>, który owija IEnumerable<T>zwrócony przez IteratorBlockale przeliczalna musi jeszcze zostać wymienione. Dzieje się tak, gdy wykonujesz foreachpętlę:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Jeśli wyliczyć wyliczenie dwa razy, to za każdym razem tworzona jest nowa instancja automatu stanów, a blok iteratora wykona dwukrotnie ten sam kod.
Należy zauważyć, że metody LINQ jak ToList(), ToArray(), First(), Count()itp użyje foreachpętli wyliczyć przeliczalnego. Na przykład ToList()wyliczy wszystkie elementy tego wyliczenia i zapisze je na liście. Możesz teraz uzyskać dostęp do listy, aby uzyskać wszystkie elementy wyliczenia bez ponownego wykonywania bloku iteratora. Występuje kompromis między wykorzystaniem procesora do wielokrotnego tworzenia elementów wyliczalnych a pamięcią do przechowywania elementów wyliczenia w celu uzyskania do nich dostępu wiele razy przy użyciu metod takich jak ToList().