yieldКлючові слова дозволяють створити IEnumerable<T>в формах на блоці ітератора . Цей блок ітераторів підтримує відкладене виконання, і якщо ви не знайомі з концепцією, це може здатися майже магічним. Однак наприкінці дня це просто код, який виконується без будь-яких дивних хитрощів.
Ітераторний блок можна описати як синтаксичний цукор, де компілятор генерує державну машину, яка відслідковує, наскільки прогресувало перерахування. Для перерахування нумерації ви часто використовуєте foreachцикл. Однак foreachпетля - це також синтаксичний цукор. Отже, ви маєте дві абстракції, вилучені з реального коду, тому спочатку може бути важко зрозуміти, як це все працює разом.
Припустимо, що у вас дуже простий блок ітератора:
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");
}
У справжніх блоках ітераторів часто є умови та петлі, але коли ви перевіряєте умови та розгортаєте цикли, вони все одно закінчуються як yield заяви, переплетені з іншим кодом.
Для перерахування блоку ітератора використовується foreachцикл:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Ось результат (сюрпризів тут немає):
Почніть
1
Після 1
2
Через 2
42
Кінець
Як зазначено вище, foreachце синтаксичний цукор:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
Намагаючись розплутати це, я створив діаграму послідовностей із видаленими абстракціями:

Державна машина, сформована компілятором, також реалізує нумератор, але щоб зробити схему більш зрозумілою, я показав їх як окремі екземпляри. (Коли стан машини перераховується з іншого потоку, ви насправді отримуєте окремі екземпляри, але ця деталь тут не важлива.)
Кожен раз, коли ви викликаєте свій блок ітераторів, створюється новий екземпляр державної машини. Однак жоден ваш код у блоці ітератора не виконується до enumerator.MoveNext()першого виконання. Ось як працює відкладене виконання. Ось (досить дурний) приклад:
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
На даний момент ітератор не виконується. WhereПоложення створює новий , IEnumerable<T>який обертає IEnumerable<T>повертається IteratorBlockале перелічуваних до сих пір не перераховані. Це відбувається під час виконання foreachциклу:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Якщо ви перераховуєте нумерацію двічі, то кожен раз створюється новий екземпляр машини, і ваш блок ітераторів виконає той самий код двічі.
Зверніть увагу , що методи LINQ подобається ToList(), ToArray(), First(), і Count()т.д. буде використовувати foreachцикл для перерахування перелічуваних. Наприклад, ToList()буде перераховано всі елементи перелічуваного і збереже їх у списку. Тепер ви можете отримати доступ до списку, щоб отримати всі елементи перелічуваного без повторного виконання блоку ітераторів. Існує компроміс між використанням процесора для отримання елементів перелічених кількох разів і пам'яттю для зберігання елементів перерахунку для доступу до них кілька разів при використанні таких методів ToList().