Ключевое слово yield
позволяет вам создать IEnumerable<T>
в форме на iterator block. Этот блок итератора поддерживает отложенное исполнение, и если вы не знакомы с концепцией, это может показаться почти магическим. Однако в конце концов это просто код, который выполняется без каких-либо странных трюков.
Блок итератора может быть описан как синтаксический сахар, где компилятор генерирует конечный автомат, который отслеживает, насколько продвинулось перечисление перечислимого. Чтобы перечислить перечислимый, вы часто используете цикл 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");
}
Real блоки итераторов часто имеют условия и циклы, но при проверке условий и раскатать петли они до сих пор в конечном итоге, как yield
заявления перемежаются с другим кодом ,
Для перечисления блока итератора используется foreach
цикл:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Вот выход (никаких сюрпризов здесь):
Begin
1
After 1
2
After 2
42
End
Как указывалось выше foreach
является синтаксический сахар:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
В попытке распутать это у меня есть cr ованная диаграмма последовательности с удаленными абстракциями:
Конечный автомат, генерируемый компилятором также реализует перечислитель, но чтобы сделать диаграмму более ясным я показал их в виде отдельных случаев. (Когда конечный автомат перечислит из другого потока, вы действительно получаете отдельные экземпляры, но эта деталь здесь не важна.)
Каждый раз, когда вы вызываете свой блок итератора, создается новый экземпляр конечного автомата. Однако ни один из ваших кодов в блоке итератора не выполняется до тех пор, пока 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()
перечислит все элементы перечислимого и сохранит их в списке. Теперь вы можете получить доступ к списку, чтобы получить все элементы перечисляемого без повторного выполнения блока итератора. Существует компромисс между использованием CPU для создания элементов перечислимого множества раз и памяти для хранения элементов перечисления для доступа к ним несколько раз при использовании таких методов, как ToList()
.
Просто ссылка MSDN об этом здесь http://msdn.microsoft.com/en-us/library/vstudio/9k7k7cf0.aspx – 2013-04-24 18:44:36
Это неудивительно. Путаница исходит из того факта, что мы обусловлены тем, что «возвращаемся» как выход функции, а впереди «выход» - нет. – Larry 2014-02-20 11:37:01
Вот очень хорошее техническое объяснение: http://blogs.msdn.com/oldnewthing/archive/2008/08/12/8849519.aspx – Nir 2008-09-02 13:21:15