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