﻿# Что такое yield и как он работает в C\#?

Возможности C\# из года в год становятся всё шире\. Разные фичи делают жизнь программиста приятнее, но предназначение и особенности некоторых из них могут быть очевидны не всем\. Например, старый\-добрый yield\. Для некоторых разработчиков, особенно начинающих, это самая настоящая магия – непонятная, но интересная\. В данной статье будет показано, как же всё\-таки работает yield, и что на самом деле скрыто за этим волшебным словом\. Приятного чтения\!

![0808_What_is_yield_and_how_does_it_work_ru/image1.png](https://import.viva64.com/docx/blog/0808_What_is_yield_and_how_does_it_work_ru/image1.png)

## Зачем нужен yield

Ключевое слово _yield_ используется для создания генераторов последовательностей элементов\. Эти генераторы не создают коллекции \- вместо этого хранится лишь текущее состояние, а по команде производится переход к следующему\. Таким образом, объём требуемой памяти оказывается минимальным и напрямую не зависит от количества элементов\. Нетрудно догадаться, что генерируемые последовательности могут быть бесконечными\.

Таким образом, генератор в самом простом случае хранит лишь некоторый текущий элемент и содержит набор команд, которые необходимо выполнить для получения нового\. Во многих случаях это гораздо удобнее, чем создавать коллекцию полностью и хранить все её элементы\.

Конечно, ничто не мешает просто написать для реализации поведения генератора собственный класс\. Однако с _yield_ делать такие генераторы намного проще\. Никаких новых классов создавать не придётся – всё будет работать, так сказать, само\.

Отмечу, что _yield_ не является фичей, доступной исключительно в C\#\. Однако несмотря на общую концепцию, реализация и особенности работы с _yield_ в различных языках могут отличаться\. Поэтому ещё раз напомню, что данная статья рассматривает _yield_ исключительно в контексте C\#\.

## Как пользоваться yield

### Общий случай

Для начала необходимо создать метод, который будет генерировать интересующую нас последовательность\. Единственное ограничение здесь состоит в том, что метод должен в качестве возвращаемого типа иметь один из представленных:

* _IEnumerable_
* _IEnumerable<T\>_
* _IEnumerator_
* _IEnumerator<T\>_

На самом деле _yield_ может использоваться не только в методах, но и в свойствах и операторах\. Однако для простоты в данной статье я буду рассматривать именно методы\.

Пояснить, как работает _yield_, проще всего на примере\. Допустим, есть у нас простенький _yield_\-метод и некоторый код, использующий его:

```cpp
static IEnumerator<int> GetInts()
{
  Console.WriteLine("first");
  yield return 1;

  Console.WriteLine("second");
  yield return 2;
}

static void Main()
{
  IEnumerator<int> intsEnumerator = GetInts(); // print nothing
  Console.WriteLine("...");                    // print "..."

  intsEnumerator.MoveNext();                   // print "first"
  Console.WriteLine(intsEnumerator.Current);   // print 1
}
```

Очевидно, вызов функции _GetInts_ вернёт объект, реализующий _IEnumerator<int\>_\. При этом код метода выполняться НЕ будет\. Можно сказать, выполнение метода будет приостановлено в самом начале\.

При первом вызове _MoveNext_ выполнение будет запущено\. Инструкции будут выполняться вплоть до первого _yield return_\. После этого выполнение опять приостановится, а в свойство _Current_ будет записано значение, указанное возле _yield return_\.

Таким образом, в результате выполнения этого кода будет выведено "\.\.\.", затем "first", а в конце 1 \- значение, записанное в свойство _Current_\.

Нетрудно догадаться, что если вызвать _MoveNext_ ещё раз, то выполнение метода вновь продолжится с того момента, где ранее было приостановлено\. Соответственно, будет выведено сообщение "second", а в свойство _Current_ будет записана 2\.

Как уже было отмечено ранее, вызовы _MoveNext_ запускают выполнение метода с момента, где оно было ранее приостановлено\. Если во время выполнения будет достигнут конец метода, то текущий вызов _MoveNext_ вернёт _false_\. Дальнейшие вызовы не будут производить никаких действий и также вернут _false_\.

Если вызвать метод _GetInts_ ещё раз, то будет возвращён новый объект, который позволит вновь выполнить генерацию элементов\.

### Локальные переменные, поля и свойства

Локальные переменные, объявленные внутри _yield_\-методов, сохраняют свои значения между вызовами _MoveNext_\. Например:

```cpp
IEnumerator<double> GetNumbers()
{
  string stringToPrint = "moveNext";
  Console.WriteLine(stringToPrint);  // print "moveNext"
  yield return 0;
  Console.WriteLine(stringToPrint);  // print "moveNext"
  stringToPrint = "anotherStr";
  yield return 1;
  Console.WriteLine(stringToPrint);  // print "anotherStr"
}
```

Если у возвращённого методом _GetNumbers_ генератора вызывать _MoveNext_, то сначала дважды будет выводиться "moveNext", а затем \- "anotherStr"\. Такое поведение в целом ожидаемо и логично\. 

А вот с полями и свойствами может возникнуть неожиданность\. Например:

```cpp
string message = "message1";

IEnumerator<int> GetNumbers()
{
  Console.WriteLine(message);
  yield return 0;
  Console.WriteLine(message);
  yield return 1;
  Console.WriteLine(message);
}
void Method()
{
  var generator = GetNumbers();
  generator.MoveNext(); // print "message1"
  generator.MoveNext(); // print "message1"
  message = "message2";
  generator.MoveNext(); // print "message2"
}
```

Так как метод _GetNumbers_ обращается к полю, то изменение его значения влияет и на логику генерации последовательности\. Причём в данном примере можно заметить, что значение поля было изменено буквально во время генерации последовательности\.

Со свойствами картина аналогичная: если возвращаемое свойством значение изменится, то это может повлиять и на генерируемую последовательность\.

### yield break

Помимо _yield return_ существует также и конструкция _yield break_, позволяющая прервать генерацию последовательности, то есть остановить генератор насовсем\. Вызов _MoveNext_, при котором будет выполнен _yield break_, вернёт _false_\. Очевидно, что никакого рода изменения полей или свойств не заставят генератор снова работать\. Совсем другое дело, если метод, использующий _yield_, будет вызван заново – ведь при этом будет создан новый объект\-генератор, который ещё не успел 'наткнуться' на _yield break_\.

Давайте рассмотрим небольшой пример генератора, использующего _yield break_:

```cpp
IEnumerator<int> GenerateMultiplicationTable(int maxValue)
{
  for (int i = 2; i <= 10; i++)
  {
    for (int j = 2; j <= 10; j++)
    {
      int result = i * j;

      if (result > maxValue)
        yield break;

      yield return result;
    }
  }
}
```

Метод возвращает последовательность результатов умножений чисел от 2 до 10 друг на друга\. При этом если произведение превышает определённый лимит \(параметр _maxValue_\), то генерация последовательности прекращается\. Данный генератор ведёт себя так именно благодаря использованию конструкции _yield break_\.

### Возвращение IEnumerable

Как было сказано в самом начале, метод, использующий yield, может возвращать _IEnumerable_, то есть как бы саму последовательность, а не её итератор\. Довольно часто более удобным вариантом может оказаться работа именно с _IEnumerable_, так как для этого интерфейса есть множество методов расширения, а также присутствует возможность обхода в цикле _foreach_\.

_Примечание\. _На самом деле, если возвращаемым типом метода будет _IEnumerable_, то фактический объект будет реализовывать и _IEnumerable_, и _IEnumerator_\. Однако приводить его к _IEnumerator_ не стоит :\)\. Почему? Расскажу, когда полезем под капот всей этой системы\.

А пока рассмотрим пример:

```cpp
void PrintFibonacci()
{
  Console.WriteLine("Fibonacci numbers:");

  foreach (int number in GetFibonacci(5))
  {
    Console.WriteLine(number);
  }
}

IEnumerable<int> GetFibonacci(int maxValue)
{
  int previous = 0;
  int current = 1;

  while (current <= maxValue)
  {
    yield return current;

    int newCurrent = previous + current;
    previous = current;
    current = newCurrent;
  }
}
```

Метод _GetFibonacci_ возвращает последовательность Фибоначчи, первые два элемента в которой равны 1\. Тот факт, что возвращаемым типом является _IEnumerable_, даёт возможность обхода элементов последовательности в цикле _foreach_\. Этой возможностью и пользуется метод _PrintFibonacci_\.

Важно помнить, что каждый раз при обходе этого _IEnumerable_ функция будет выполняться заново\. Дело в том, что _foreach_ использует _GetEnumerator_ для обхода элементов последовательности\. Каждый новый вызов _GetEnumerator_ вернет объект, производящий генерацию последовательности с самого её начала\. Например:

```cpp
int _rangeStart;
int _rangeEnd;

void TestIEnumerableYield()
{
  IEnumerable<int> polymorphRange = GetRange();

  _rangeStart = 0;
  _rangeEnd = 3;

  Console.WriteLine(string.Join(' ', polymorphRange)); // 0 1 2 3

  _rangeStart = 5;
  _rangeEnd = 7;

  Console.WriteLine(string.Join(' ', polymorphRange)); // 5 6 7
}

IEnumerable<int> GetRange()
{
  for (int i = _rangeStart; i <= _rangeEnd; i++)
  {
    yield return i;
  }
}
```

При первом вызове _string\.Join_ будет произведён первый обход _IEnumerable_, в результате которого будет произведено выполнение кода из метода _GetRange_\. Похожего результата можно было бы добиться, к примеру, используя цикл _foreach_\. Перед вторым вызовом значения полей _\_rangeStart_ и _\_rangeEnd_ переопределяются и о чудо – обход **того же самого** _IEnumerable_ даёт уже другой результат\! 

Если вы знакомы с LINQ, то подобное поведение, возможно, не будет казаться чем\-то необычным, ведь работа с результатами LINQ\-запросов строится аналогичным образом\. Однако менее опытных разработчиков такие чудеса могут поставить в тупик\. Так или иначе, стоит учитывать эту особенность\.

Помимо того, что повторные обходы могут выдавать неожиданные результаты, есть и другая проблема\. Дело в том, что все операции, выполнявшиеся для формирования элементов, будут выполняться повторно\. Это может негативно сказаться на производительности приложения\. 

## Когда пользоваться yield

В зависимости от ситуации и конкретного проекта, _yield_ может использоваться повсеместно или не использоваться вообще\. Помимо очевидных вариантов, эта конструкция может быть полезна, когда необходимо реализовать условно параллельное выполнение нескольких методов\. Достаточно активно эту концепцию практикуют в игровом движке Unity\.

Как правило, нет смысла использовать _yield_, например, для простой фильтрации или преобразования элементов существующей коллекции – с этим в большинстве случаев прекрасно справится LINQ\. Однако _yield_ позволяет генерировать последовательности элементов, которые, на самом деле, ни к какой коллекции не принадлежат\. Например, при работе с деревом может быть удобной функция, которая перебирает предков конкретного узла:

```cpp
public IEnumerable<SyntaxNode> EnumerateAncestors(SyntaxNode node)
{
  while (node != null)
  { 
    node = node.Parent;
    yield return node;
  }
}
```

Этот метод позволяет перебирать предков, начиная от самого близкого\. При этом не производится создание каких\-либо коллекций, а генерация элементов может быть прекращена досрочно – например, если производится поиск конкретного предка\. Если у вас есть идеи, как можно реализовать такое поведение без использования _yield_ \(и хотя бы в некоторой степени лаконично\), то всегда жду вас в комментариях :\)\. 

## Ограничения

При всей широте возможностей _yield_ имеет ряд ограничений, связанных, в первую очередь, с внутренней реализацией\. Некоторые из этих ограничений будут поясняться в следующем разделе, где мы наконец\-то посмотрим, за счёт чего вся эта магия работает\. Сейчас же давайте просто рассмотрим список тех самых ограничений:

* несмотря на то, что интерфейс _IEnumerator_ содержит метод _Reset_, объект, возвращаемый _yield_\-методом, не имеет его корректной реализации\. При попытке вызова _Reset_ у этого объекта будет выброшено исключение типа _NotSupportedException_\. Будьте осторожны с этим: не передавайте объект\-генератор в методы, которые могут вызвать у него _Reset_;
* _yield_ нельзя использовать в анонимных методах или лямбда\-выражениях;
* _yield_ нельзя использовать в методах, содержащих unsafe\-код;
* конструкцию _yield return_ нельзя использовать внутри блока _try\-catch_\. Однако это ограничение не касается секций _try_ блоков _try\-finally_\. _yield break_ можно использовать в секциях _try_ как _try\-catch_, так и _try\-finally_ блоков\.

## Как же оно всё\-таки работает?

Увидеть, во что превращаются _yield_\-методы, поможет утилита dotPeek\. Снова рассмотрим функцию _GetFibonacci_, возвращающую последовательность Фибоначчи с ограничением в _maxValue_:

```cpp
IEnumerable<int> GetFibonacci(int maxValue)
{
  int previous = 0;
  int current = 1;

  while (current <= maxValue)
  {
    yield return current;

    int newCurrent = previous + current;
    previous = current;
    current = newCurrent;
  }
}
```

Активировав настройку 'Show compiler\-generated code', произведём декомпиляцию приложения с помощью dotPeek\. Как же выглядит метод _GetFibonacci_ на самом деле?

Ну, как\-то так:

```cpp
[IteratorStateMachine(typeof(Program.<GetFibonacci>d__1))]
private IEnumerable<int> GetFibonacci(int maxValue)
{
  <GetFibonacci>d__1 getFibonacciD1 = new <GetFibonacci>d__1(-2);
  getFibonacciD1.<>4__this = this;
  getFibonacciD1.<>3__maxValue = maxValue;
  return (IEnumerable<int>)getFibonacciD1;
}
```

Практически ничего общего с исходным методом, не так ли? Не говоря уж о том, что написано всё это несколько странным образом\. Что ж, давайте разбираться\.

Сперва переведём всё это дело на понятный язык \(нет, не на IL\):

```cpp
[IteratorStateMachine(typeof(GetFibonacci_generator))]
private IEnumerable<int> GetFibonacci(int maxValue)
{
  GetFibonacci_generator generator = new GetFibonacci_generator(-2);
  generator.forThis = this;
  generator.param_maxValue = maxValue;
  return generator;
}
```

По сути, это тот же самый код\. Отличие заключаются в более приятных глазу названиях и отсутствии избыточных в данном случае конструкций\. Кроме того, этот код, в отличие от показанного ранее, нормально воспринимается C\#\-компилятором\. Далее в статье будет использоваться именно такая форма\. Если у вас есть желание увидеть, как же оно выглядит безо всяких прикрас, то хватайте dotPeek \(или ещё лучше – ildasm\) и вперёд :\)\. 

Здесь создаётся специальный объект, в который сохраняется ссылка на текущий экземпляр, а также значение параметра _maxValue_\. В конструктор передаётся '\-2' – это, как мы увидим далее, начальное состояние генератора\.

Класс генератора был создан компилятором автоматически, и вся логика, которую мы заложили в функцию, реализована там\. Давайте же поглядим, из чего состоит этот класс\.

Начнём с объявления:

```cpp
class GetFibonacci_generator : IEnumerable<int>,
                               IEnumerable,
                               IEnumerator<int>,
                               IEnumerator,
                               IDisposable
```

В принципе, ничего неожиданного\.\.\. Кроме неожиданно появившегося _IDisposable_\! Кроме того, может показаться странным, что класс реализует _IEnumerator_, хотя метод _GetFibonacci_ возвращает _IEnumerable<int\>_\. Ну что же, давайте разбираться\.

Рассмотрим конструктор:

```cpp
public GetFibonacci_generator(int startState)
{
  state = startState;
  initialThreadId = Environment.CurrentManagedThreadId;
}
```

Код состояния, переданный при создании объекта, то есть '\-2', сохраняется в поле\. Кроме того, сохраняется идентификатор потока, в котором объект был создан\. Назначение этих полей станет понятным далее, а сейчас взглянем на реализацию _GetEnumerator_:

```cpp
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
  GetFibonacci_generator generator;
  
  if (state == -2 && initialThreadId == Environment.CurrentManagedThreadId)
  {
    state = 0;
    generator = this;
  }
  else
  {
    generator = new GetFibonacci_generator(0);
    generator.forThis = forThis;
  }
  
  generator.local_maxValue = param_maxValue;
  
  return generator;
}
```

Обратите внимание, что при выполнении определённых условий метод вернёт не новый объект, а тот же самый\. Эта особенность может показаться достаточно неожиданной\.  Подтвердить её поможет следующий фрагмент кода: 

```cpp
IEnumerable<int> enumerable = prog.GetFibonacci(5);
IEnumerator<int> enumerator = enumerable.GetEnumerator();

Console.WriteLine(enumerable == enumerator);
```

Удивительно, но при выполнении этого кода будет выведено 'True'\. Кто бы мог подумать? :\)

Важно отметить, что при вызове _GetEnumerator_ в поле _state_ возвращаемого объекта будет записан '0'\. Как мы увидим далее, это достаточно важный момент\.

После блока условия также производится важное присваивание:

```cpp
generator.local_maxValue = param_maxValue
```

Если вернуться к методу _GetFibonacci _\(вернее, к тому, во что его превратил компилятор\), то можно заметить, что в _param\_maxValue_ записано значение соответствующего параметра\. Оно же записывается и в поле _local\_maxValue_\.

Может показаться странным, что для одного и того же параметра _maxValue_ в генераторе выделено целых 2 поля – _param\_maxValue_ и _local\_maxValue_\. Пока что нам не хватает информации для того, чтобы прояснить этот момент, однако довольно скоро мы вернёмся к нему\. Сейчас же давайте рассмотрим метод _MoveNext_:

```cpp
bool IEnumerator.MoveNext()
{
  switch (state)
  {
    case 0:
      state = -1;
      local_previous = 0;
      local_current = 1;
      break;
    case 1:
      state = -1;
      local_newCurrent = local_previous + local_current;
      local_previous = local_current;
      local_current = local_newCurrent;
      break;
    default:
      return false;
  }
  
  if (local_current > local_maxValue)
    return false;
  
  _current = local_current;
  state = 1;
  
  return true;
}
```

Именно тут реализована вся логика, которую мы заложили при написании метода _GetFibonacci_\. Перед завершением работы _MoveNext_ записывает текущий результат в поле _\_current_\. Именно это значение мы получим при обращении к свойству _Current_ генератора последовательности\.

Если генерация последовательности должна быть закончена \(в данном случае при _local\_current \> local\_maxValue_\), то состояние генератора остаётся равным '\-1'\. Генератор с таким значением поля _state_ перестаёт работать – _MoveNext_ не будет производить каких\-либо действий и просто вернёт _false_\.

Стоит также обратить внимание, что в случаях, когда _MoveNext_ возвращает _false_, значение поля _\_current_ \(а, следовательно, и свойства _Current_\) остаётся неизменным\.

### Фокусы с приведением типов

Вернёмся немного назад\. При создании генератора в поле _state _записывается значение '\-2'\. Но по коду видно, что если _state_ _\= \-2_, то _MoveNext_ не будет выполнять каких\-либо действий и попросту вернёт _false_\. По сути, генератор не будет работать\. К счастью, состояние '\-2' заменяется на '0' при вызове метода _GetEnumerator_\. А можно ли вызвать _MoveNext_, не вызывая при этом _GetEnumerator_?

Возвращаемый тип метода _GetFibonacci_ – _IEnumerable_, следовательно, доступ к методу _MoveNext _отсутствует\. Тем не менее, зная, что фактически полученный объект будет реализовывать не только _IEnumerable_, но и _IEnumerator_, можно воспользоваться приведением типов\. В этом случае у разработчика будет возможность вызывать у генератора _MoveNext_, не прибегая к _GetEnumerator_, вот только\.\.\. Все вызовы вернут _false_\. Таким образом, 'обмануть' систему вроде бы и можно, да только ничего это вам не даст\.

_Вывод_\. _yield_\-метод, возвращающий _IEnumerable_, фактически вернёт объект, который реализует и _IEnumerable_, и _IEnumerator_\. Приведение такого объекта к _IEnumerator_ даст генератор, который будет бесполезен вплоть до момента, пока не вызовется _GetEnumerator_\. В то же время, после такого вызова генератор, казавшийся 'мёртвым', ни с того ни с сего начнёт работать\. Данное поведение демонстрирует следующий код:

```cpp
IEnumerable<int> enumerable = GetFibonacci(5);
IEnumerator<int> deadEnumerator = (IEnumerator<int>)enumerable;

for (int i = 0; i < 5; ++i)
{
  if (deadEnumerator.MoveNext())
  {
    Console.WriteLine(deadEnumerator.Current);
  }
  else
  {
    Console.WriteLine("Sorry, your enumerator is dead :(");
  }
}

IEnumerator<int> enumerator = enumerable.GetEnumerator();
Console.WriteLine(deadEnumerator == enumerator);

for (int i = 0; i < 5; ++i)
{
  if (deadEnumerator.MoveNext())
  {
    Console.WriteLine(deadEnumerator.Current);
  }
  else
  {
    Console.WriteLine("Sorry, your enumerator is dead :(");
  }
}
```

Как считаете, что будет выведено в окно консоли при выполнении всех указанных команд? Подсказка: в данной реализации первые 5 элементов последовательности Фибоначчи это 1, 1, 2, 3, 5\.

Мы рассмотрели с вами случай приведения к _IEnumerator_\. А можно ли поиграться с приведением к _IEnumerable_?

Объект, возвращённый при первом вызов _GetEnumerator_, очевидно, будет без проблем приводиться к _IEnumerable_ и работать как подобает\. Это становится совсем очевидным, если взглянуть на код:

```cpp
IEnumerable<int> enumerable = GetInts(0);                     
IEnumerator<int> firstEnumerator = enumerable.GetEnumerator();
IEnumerable<int> firstConverted = (IEnumerable<int>)firstEnumerator;

Console.WriteLine(enumerable == firstEnumerator);
Console.WriteLine(firstConverted == firstEnumerator);
Console.WriteLine(firstConverted == enumerable);
```

В результате выполнения будет произведён вывод трёх 'True' в окно консоли, так как все три ссылки фактически указывают на один и тот же объект\. Следовательно, приведение не принесёт сюрпризов, а попросту даст ссылку на уже существующий \(а значит \- корректно работающий\) объект\.

Но что же случится, если преобразовать к _IEnumerable_ результат второго вызова _GetEnumerator_ \(ну или вызова, производимого в другом потоке\)? С этим вопросом, давайте рассмотрим другой _yield_\-метод:

```cpp
IEnumerable<string> RepeatLowerString(string someString)
{
  someString.ToLower();

  while (true)
  {
    yield return someString;
  }
}
```

Очевидно, метод приводит полученную строку к нижнему регистру и затем бесконечно её возвращает\. Вроде всё просто\. 

Хм, а вы заметили странность в коде выше? Метод _RepeatLowerString_, судя по названию, должен генерировать последовательность, состоящую из ссылок на переданную строку нижнем регистре\. А что в итоге?

Верно, вызов _ToLower_ ни на что тут не повлияет, так как он вообще\-то не меняет исходную строку, а создаёт новую\. Конечно, в нашем случае это не так уж важно, но в реальной практике ошибки подобного плана приводят к печальным последствиям, и с ними стоит бороться\. Некорректный вызов _ToLower_, может, и не кажется особенно страшным, но куда большие проблемы могут возникнуть из\-за скрытой в большой куче кода другой ошибки, связанной с "лишним" вызовом какой\-нибудь другой функции\.

В таких случаях на более\-менее больших проектах часто используется статический анализатор\. Это такое приложение, которое позволяет найти большое количество ошибок в коде за достаточно короткий промежуток времени\. К примеру, статический анализатор легко бы смог найти ту ошибку в коде метода _RepeatLowerString_, о которой мы говорили ранее\. Хотя также стоит отметить, что спектр обнаруживаемых анализатором ошибок не ограничивается одними лишь "бессмысленными вызовами" – он намного, намного шире\. 

В общем, рекомендую и вам использовать статический анализатор на своих проектах\. При выборе конкретного приложения неплохим вариантом является PVS\-Studio\. Он находит достаточно много проблем, скрытых в исходниках, а также позволяет проверять код не только на C\#, но и на C, C\+\+ и Java\. Если заинтересовались, то можете перейти на официальный сайт PVS\-Studio по [ссылке](https://pvs-studio.ru/ru/pvs-studio/download/) и совершенно бесплатно попробовать использовать анализатор в течение пробного периода\.

Ну а я, тем временем, подправил метод _RepeatLowerString_:

```cpp
IEnumerable<string> RepeatLowerString(string someString)
{
  string lower = someString.ToLower();

  while (true)
  {
    yield return lower;
  }
}
```

Что ж, давайте теперь проведём эксперимент с приведением к _IEnumerable_:

```cpp
IEnumerable<string> enumerable = RepeatLowerString("MyString");
IEnumerator<string> firstEnumerator = enumerable.GetEnumerator();

IEnumerator<string> secondEnumerator = enumerable.GetEnumerator();
var secondConverted = (IEnumerable<string>)secondEnumerator;

var magicEnumerator = secondConverted.GetEnumerator();

for (int i = 0; i < 5; i++)
{
  magicEnumerator.MoveNext();
  Console.WriteLine(magicEnumerator.Current);
}
```

Что будет выведено в окно консоли при выполнении данного фрагмента?

![0808_What_is_yield_and_how_does_it_work_ru/image2.png](https://import.viva64.com/docx/blog/0808_What_is_yield_and_how_does_it_work_ru/image2.png)

Ничего\! До вывода строки на экран дело не дойдёт, так как вся эта конструкция завалится с _NullReferenceException_\. Неожиданно?

Ну, может и нет\. На самом деле мы уже располагаем достаточной информацией для того, чтобы объяснить такое поведение\. Тем не менее, давайте всё же этот момент подробно разберём\.

Исключение было выброшено, когда _magicEnumerator\.MoveNext\(\)_ привел к вызову метода _ToLower_\. Он вызывается у параметра _someString_, который внутри генератора условно представлен полями _param\_someString_ и _local\_someString_:

```cpp
public string param_someString;
private string local_someString;
```

При этом, как вы, вероятно, помните, метод _MoveNext_, внутри которого и было выброшено исключение, работает именно с полем _local\_someString_:

```cpp
bool IEnumerator.MoveNext()
{
  switch (this.state)
  {
    case 0:
      this.state = -1;
      this.local_lower = this.local_someString.ToLower();
      break;
    case 1:
      this.state = -1;
      break;
    default:
      return false;
  }
  this._current = this.local_lower;
  this.state = 1;
  return true;
}
```

Следовательно, _null_ был записан в него\. Но откуда он там взялся?

При вызове _GetEnumerator_ в поле _local\_someString_ возвращаемого объекта всегда записывается значение из _param\_someString_:

```cpp
IEnumerator<string> IEnumerable<string>.GetEnumerator()
{
  RepeatLowerString_generator generator;
  
  if (state == -2 && initialThreadId == Environment.CurrentManagedThreadId)
  {
    state = 0;
    generator = this;
  }
  else
  {
    generator = new RepeatLowerString_generator(0);
    generator.forThis = forThis;
  }
  
  generator.local_someString = param_someString;
  
  return generator;
}
```

Стало быть, _null_ пришёл оттуда? Так и есть\. Но почему же в этом поле оказался _null_? Давайте\-ка взглянем на фрагмент кода ещё разок:

```cpp
IEnumerable<string> enumerable = RepeatLowerString("MyString");
IEnumerator<string> firstEnumerator = enumerable.GetEnumerator();

IEnumerator<string> secondEnumerator = enumerable.GetEnumerator();
var secondConverted = (IEnumerable<string>)secondEnumerator;

var magicEnumerator = secondConverted.GetEnumerator();

for (int i = 0; i < 5; i++)
{
  magicEnumerator.MoveNext(); // NRE
  Console.WriteLine(magicEnumerator.Current);
}
```

При втором вызове _GetEnumerator_ мы получим новый объект, в котором значение поля _local\_SomeString _будет задано корректно\. А будет ли задано значение _param\_someString_? Увы, но нет – метод _GetEnumerator_ этого не делает\. Получается, в этом поле будет записано значение по умолчанию – то есть, тот самый _null_\. 

А ведь именно поле _param\_someString_ будет использовано для задания значения _local\_someString_ у объекта _magicEnumerator_\! А исключение было выброшено как раз при попытке вызова _local\_someString\.ToLower\(\)_\.

_Вывод_\. Если _GetEnumerator_ возвращает не _this_, то полученный объект не сможет полноценно выполнять роль _IEnumerable_\. Проблема состоит в том, что у такого объекта не будут заданы необходимые для корректной работы значения полей _param\_\*_\. В то же время это не актуально для _yield_\-методов, которые не принимают каких\-либо параметров\. Например:

```cpp
IEnumerable<int> GetPositive()
{
  int i = 0;
  
  while (true)
    yield return ++i;
}
```

Метод возвращает возрастающую последовательность положительных чисел, начиная с 1\. А теперь взгляните на пример его использования:

```cpp
IEnumerable<int> enumerable = GetPositive();
IEnumerator<int> firstEnumerator = enumerable.GetEnumerator();

IEnumerator<int> secondEnumerator = enumerable.GetEnumerator();
var secondConverted = (IEnumerable<int>)secondEnumerator;

IEnumerator<int> magicEnumerator = secondConverted.GetEnumerator();

for (int i = 0; i < 5; i++)
{
  magicEnumerator.MoveNext();
  Console.WriteLine(magicEnumerator.Current);
}
```

Данный код отработает без проблем и выведет на экран числа от 1 до 5\. Но лучше всё равно так не делать, хехех :\)\.

### 2 поля для одного параметра

При рассмотрении сгенерированного класса неизбежно возникает вопрос – почему для хранения значения параметра выделяется два поля, а не одно\. Возможно, к этому моменту вы уже догадались, в чём здесь дело, но на всякий случай давайте разберём этот момент подробнее\.

Для наглядности напишем другой yield\-метод:

```cpp
IEnumerable<int> GetInts(int i)
{
  while (true)
  {
    yield return i++;
  }
}
```

Весьма простенький метод, позволяющий получить возрастающую последовательность целых чисел, начиная с передаваемого _i_\. Метод _MoveNext_ созданного генератора выглядит примерно так:

```cpp
bool IEnumerator.MoveNext()
{
  switch (this.state)
  {
    case 0:
      this.state = -1;
      break;
    case 1:
      this.state = -1;
      break;
    default:
      return false;
  }
  this._current = this.local_i++;
  this.state = 1;
  return true;
}
```

В данном коде важно заметить, что значение, записанное в поле _local\_i_, меняется каждый раз при вызове _MoveNext_\. Теперь вспомним, что исходное значение этого поля устанавливается при вызове _GetEnumerator_ – оно берётся из второго поля – в данном случае, _param\_i_:

```cpp
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
  GetInts_generator generator;
  
  if (   state == -2 
      && initialThreadId == Environment.CurrentManagedThreadId)
  {
    state = 0;
    generator = this;
  }
  else
  {
    generator = new GetInts_generator(0);
    generator.forThis = forThis;
  }
  
  generator.local_i = param_i;
  
  return generator;
}
```

Значение _param\_i_, в свою очередь, задаётся при вызове исходного _yield_\-метода _GetInts_:

```cpp
[IteratorStateMachine(typeof(GetInts_generator))]
private IEnumerable<int> GetInts(int i)
{
  GetInts_generator generator = new GetInts_generator(-2);
  generator.forThis = this;
  generator.param_i = i;
  return generator;
}
```

После этого оно никогда не меняется\. И всё\-таки зачем здесь нужно поле _param\_i_? Почему бы, к примеру, не присваивать значение сразу в _local\_i_?

Возвращаемый тип объявленного нами ранее _yield_\-метода _GetInts_ – _IEnumerable_\. У объекта такого типа можно несколько раз вызвать _GetEnumerator_\. Как мы знаем, при первом вызове генератор вернёт себя же\. С этой мыслью, давайте взглянем на следующий код:

```cpp
IEnumerable<int> enumerable = GetInts(0);
// enumerable.param_i = 0

IEnumerator<int> firstEnumerator = enumerable.GetEnumerator(); 
// firstEnumerator.local_i = enumerable.param_i

Console.WriteLine(enumerable == firstEnumerator); // True

firstEnumerator.MoveNext(); 
// firstEnumerator.local_i++
firstEnumerator.MoveNext(); 
// firstEnumerator.local_i++

IEnumerator<int> secondEnumerator = enumerable.GetEnumerator(); 
// secondEnumerator.local_i = ?
```

В первой строке производится вызов _GetInts_, возвращающий экземпляр класса\-генератора\. При этом в его поле _param\_i_ записывается переданный нами аргумент – '0'\. Далее мы получаем _firstEnumerator_\. В соответствии со сказанным ранее, фактически это будет тот же самый объект, что и _enumerable_\. Отметим также, что при вызове _GetEnumerator_ полю _local\_i_ возвращаемого объекта присваивается значение поля _param\_i_ объекта _enumerable_\. 

Ниже производится пара вызовов _MoveNext_\. Это приводит к изменению значения поля _local\_i_, причём как у _firstEnumerator_, так и у_ enumerable_, ведь эти ссылки указывают на один и тот же объект\.

В последней части представленного фрагмента производится получение второго _IEnumerator_\. Как вы считаете, каким значением должно быть проинициализировано его поле _local\_i_? Очевидно, тем самым, что было передано в _yield_\-метод изначально\.

Именно его и хранит поле _param\_i_\. Вне зависимости от того, как значение _local\_i_ будет меняться при вызовах _MoveNext_, поле _param\_i_ остаётся неизменным\. Как мы видели ранее, значение этого поля записывается в поле _local\_i_ объекта, возвращаемого при вызове _GetEnumerator_\.

_Вывод\._ Объекты, возвращаемые при вызове _GetEnumerator_, в определённой степени независимы друг от друга\. Они начинают генерировать последовательности, используя значения параметров, которые были переданы при вызове _yield_\-метода\. Достигается это благодаря хранению исходного значения параметра в дополнительном поле\.

### Возвращение IEnumerator

Выше мы рассмотрели несколько особенностей генераторов, классы которых построены на основе _yield_\-методов, возвращающих _IEnumerable_\. Все они так или иначе связаны с тем, что класс генератора реализует и _IEnumerator_, и _IEnumerable_\. Куда проще всё обстоит с классами, генерирующимися на основе методов, которые возвращают _IEnumerator_\.

Дело в том, что в этом случае генерируемый класс не будет реализовывать _IEnumerable_\. Соответственно, рассмотренных ранее фокусов с приведением типов здесь уже не выйдет\. Ниже перечислены основные отличия классов, генерируемых для _yield_\-метода, возвращающего _IEnumerator_ и _yield_\-метода, возвращающего _IEnumerable_:

* отсутствие метода _GetEnumerator;_
* отсутствие поля _initialThreadId;_
* использование одного поля для хранения значения параметра вместо двух\.

Кроме того, небольшое отличие есть и в процессе создания генераторов\. Возможно, вы помните, что при создании экземпляра генератора для _yield_\-метода, возвращающего _IEnumerable_, в поле _state_ записывалось значение '\-2' и менялось оно лишь при вызове _GetEnumerator_\. При таком значении _state_ вызов _MoveNext_ просто возвращает _false_ без выполнения каких\-либо действий\. 

Если генератор создавался для метода, возвращающего _IEnumerator_, то никакого _GetEnumerator_ у него нет\. Поэтому '0' записывается в поле _state_ сразу при создании экземпляра\.

### Зачем генератор реализует Dispose

Генератор вынужден реализовывать _Dispose_ из\-за того, что _IEnumerable<T\>_ наследует _IDisposable_\. В общем случае метод _Dispose_ сформированного класса пуст\. Однако есть ситуации, когда _Dispose_ всё же содержит код\. Связаны эти ситуации с использованием оператора _using_\.

Взгляните на следующие конструкции:

```cpp
using (var disposableVar = CreateDisposableObject())
{
  ....
}
```



```cpp
using var disposableVar = CreateDisposableObject();
....
```

Они обеспечивают гарантированный вызов метода _Dispose_ у объекта _disposableVar_ либо при выходе из соответствующего блока \(первый пример\), либо при выходе из метода \(второй пример\)\. Подробнее о _using_ можно прочесть в [официальной документации](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement)\.

Наличие _using_ в _yield_\-методе влияет на формируемый класс генератора соответствующим образом\. В частности, у объектов, фигурирующих в конструкции _using_, в нужные моменты будет вызываться _Dispose_\. При этом, в соответствии с поведением, ожидаемым от оператора, _Dispose_ будет вызван даже в случае, если во время выполнения было выброшено исключение\.

Нетрудно догадаться, что метод _Dispose_ самого генератора производит вызовы _Dispose_ для всех соответствующих полей\. В частности, для тех, что представляют локальные переменные, использующиеся с _using_ в исходном _yield_\-методе\.

Рассмотрим простой пример:

```cpp
static IEnumerable<string> GetLines(string path)
{
  using (var reader = new StreamReader(path))
  {
    while (!reader.EndOfStream)
      yield return reader.ReadLine();
  }
}
```

Данный метод возвращает объект, позволяющий построчно считывать информацию из файла\. Наличие конструкции _using _не влияет на содержимое метода _GetEnumerator_, однако приводит к появлению нового метода:

```cpp
private void Finally1()
{
  this.state = -1;
  if (this.local_reader == null)
    return;
  this.local_reader.Dispose();
}
```

Отметим, что после вызова _Dispose_ полю _state_ присваивается значение, при котором дальнейшие вызовы _MoveNext_ \(его рассмотрим чуть дальше\) не будут производить каких\-либо действий и просто вернут _false_\.

На самом деле такой _finally_\-метод не обязательно будет один – использование нескольких конструкций _using_ приведёт к добавлению похожих методов и усложнению структуры _MoveNext_ и _Dispose_\. В данном простом случае метод _Dispose_ выглядит вполне тривиально:

```cpp
void IDisposable.Dispose()
{
  switch (this.state)
  {
    case -3:
    case 1:
      try
      {
      }
      finally
      {
        this.Finally1();
      }
      break;
  }
}
```

Данная конструкция выглядит избыточной, однако усложнение структуры исходного метода и использование в нём нескольких _using_ сразу наполнят её смыслом \(и, скорее всего, сделают сложнее\)\. Если заинтересовались, то предлагаю вам поэкспериментировать с этим самостоятельно :\)\.

Вызов _Dispose_ у генератора может иметь смысл в случае, когда необходимо прервать генерацию последовательности и освободить используемые ресурсы\. Возможно, есть и другие ситуации, когда такой вызов и само наследование _IDisposable_ будет полезным\. Если у вас есть идеи по этому поводу, то напишите их, пожалуйста, в комментариях\.

Наконец, давайте мельком взглянем на _MoveNext_:

```cpp
bool IEnumerator.MoveNext()
{
  try
  {
    switch (this.state)
    {
      case 0:
        this.state = -1;
        this.local_reader = new StreamReader(this.local_path);
        this.state = -3;
        break;
      case 1:
        this.state = -3;
        break;
      default:
        return false;
    }
    if (!this.local_reader.EndOfStream)
    {
      this._current = this.local_reader.ReadLine();
      this.state = 1;
      return true;
    }
    this.Finally1();
    this.local_reader = null;
    return false;
  }
  fault
  {
    Dispose();
  }
}
```

В данном коде реализуется поведение, ожидаемое при использовании оператора _using_ в написанном _yield_\-методе\. Обратите внимание на конструкцию _fault_\. На самом деле C\# на момент написания статьи такую конструкцию не поддерживает, однако она используется в IL\-коде\. В самом простом случае это работает так: если в блоке _try_ будет выброшено исключение, то выполнятся инструкции, указанные в _fault_\. Хотя тут, надо полагать, всё не так просто\! А как вы считаете? Приглашаю вас поделиться своими мыслями по поводу особенностей _fault_ в комментариях :\)\.

Таким образом, можно быть уверенным в том, что _Dispose_ будет вызван у всех переменных, объявляемых через _using_, причём именно тогда, когда это будет нужно\. Наличие различных ошибок также не повлияет на данное поведение\.

### Не вызывайте Reset\!

Напоследок убедимся в том, что метод _Reset_ в классе генератора действительно выбрасывает исключение:

```cpp
[DebuggerHidden]
void IEnumerator.Reset()
{
  throw new NotSupportedException();
}
```

Ну что же, коротко и ясно – перед нами _NotSupportedException_\. Соответственно, нужно запомнить, что передавать генератор стоит только в те методы, в которых точно не будет произведён вызов _Reset_\. Ну или хотя бы туда, где соответствующее исключение будет корректно обработано\.

## Заключение

В данной статье я постарался максимально полно разобрать информацию, касающуюся использования _yield_ в C\#\. Мы рассмотрели самые различные кейсы – от простейших болванок до методов с циклами и ветвлениями, разобрали случаи, когда _yield_ удобен, а когда он не очень\-то нужен и даже поглядели 'под капот', углубив своё понимание происходящего и разобравшись в некоторой магии\.

В разделе 'Ограничения' было упомянуто, что _yield return_ нельзя использовать внутри блоков _try\-catch_\. Теперь, когда вы знаете, что же на самом деле представляют из себя _yield_\-методы, вы можете поразмышлять над причиной этого и других ограничений\. Ну а если хочется, чтобы это сделал кто\-то другой, то можно перейти по ссылкам [сюда](https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/) и [сюда](https://docs.microsoft.com/en-us/archive/blogs/ericlippert/iterator-blocks-part-four-why-no-yield-in-catch)\.

Методы, в которых используется _yield_, действительно позволяют иногда сильно упростить себе жизнь\. За этим удобством скрыт целый класс, генерируемый компилятором, поэтому применять эту фичу стоит лишь в тех случаях, когда это будет действительно приятнее, чем использовать, например, тот же LINQ\. Кроме того, важно уметь разделять случаи, когда действительно полезно 'ленивое выполнение', и случаи, когда лучше просто закинуть нужные элементы в старый\-добрый _List_ и не париться :\)\.

Если вам понравилась данная статья, то предлагаю вам подписаться на [мой Twitter](https://twitter.com/Nikita30005701) – иногда я выкладываю там посты с различными интересными моментами, которые нахожу в коде, а также анонсы статей на различные темы\.

Что ж, у меня на этом всё\. Большое спасибо за внимание и всего доброго\!