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()
.