Słowo yield
kluczowe 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 foreach
pętli. Jednakże foreach
pę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 yield
instrukcje przeplatane z innym kodem.
Do wyliczenia bloku iteratora foreach
uż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, foreach
cukier 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ę. Where
Klauzula tworzy nowy IEnumerable<T>
, który owija IEnumerable<T>
zwrócony przez IteratorBlock
ale przeliczalna musi jeszcze zostać wymienione. Dzieje się tak, gdy wykonujesz foreach
pę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 foreach
pę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()
.