Забыли пароль?

Что такое yield return?

Dmitriy Fedyashov

Оператор yield return один из самых малоизвестных среди программистов C#. По крайней мере среди начинающих. И даже те, кто о нем кое-что знает, до конца не уверены, что правильно понимают принцип его работы. Этот досадный пробел обязательно нужно исправить. И, я надеюсь, эта статья вам поможет с этим.

Оператор yield return возвращает элемент коллекции в итераторе и перемещает текущую позицию на следующий элемент. Наличие оператора yield return превращает метод в итератор. Каждый раз, когда итератор встречает yield return он возвращает значение.

Этот оператор сигнализирует нам и компилятору, что данное выражение – итератор. Задача итератора перемещаться между элементами коллекции и возвращать значение текущего. Многие привыкли называть счетчик в цикле итератором, но это не так, ведь счетчик не возвращает значение.

Итератор преобразуется компилятором в «конечный автомат», который отслеживает текущую позицию и знает, как «переместиться» на следующую позицию. При этом значение элемента последовательности вычисляется в момент обращения к нему.

Вот простейший пример итератора:

1
2
3
4
5
6
public static IEnumerable<int> GetItems()
{
 foreach (var i in List)
 {
 yield return i;
 }
}

Итераторы могут возвращать только тип IEnumerable<>.

Итераторы являются синтаксическими ярлыками для более сложного шаблона перечислителя. Когда компилятор C # встречает итератор, он расширяет его содержимое в CIL-код, который реализует шаблон перечислителя. Такая инкапсуляция существенно экономит время программиста.

Первый вопрос, который возникнет у неискушенного программиста: «Зачем мне использовать итератор? Я прекрасно могу выводить последовательность и без него».

Конечно можете. Различие в подходах. Итератор позволяет делать так называемое «ленивое вычисление». Это значит, что значение элемента вычисляется только когда он запрашивается.

Чтобы лучше понять, как работает yield return, мы сравним его с традиционными циклами. На примерах все станет понятно.

1)      Обратите внимание, что с yield return нам не нужно создавать дополнительный список, чтобы заполнить его значениями. А значит мы получаем экономию памяти, ведь нам требуется лишь память для текущего элемента коллекции. При поэлементной обработке не выделяется память, достаточно кэша.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static IEnumerable<int> GetSequence()
 {
 Random rand = new Random();
 List<int> list = new List<int>();
 for (int i = 0; i < 3; i++)
 list.Add(rand.Next());
 return list;
 }
 
 static IEnumerable<int> GetSequence()
 {
 Random rand = new Random();
 for (int i = 0; i < 3; i++)
 yield return rand.Next();
 }

2)      Возможность не вычислять результат для всего перечисления. Это главное преимущество. Вы помните, что yield return возвращает значение в момент его обработки? В этом примере мы бесконечно генерируем числа:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
IEnumerable<int> GetInfinityWithIterator()
{
var i = 0;
while (true)
yield return ++i;
}
 
IEnumerable<int> GetInfinityWithLoop()
{
 var i = 0;
 var list = new List<int>();
 while (true)
 list.Add(++i);
 return list;
}

 Вы уже видите разницу? Сейчас все поймете:

1
2
3
4
foreach(var item in GetInfinityWithIterator().Take(5))
{
 Console.WriteLine(item);
}

Мы используем LINQ оператор Take, чтобы ограничить количество выборки. В случае с yield return цикл остановится на пятом элементе.

1
2
3
4
foreach(var item in GetInfinityWithLoop().Take(5))
{
 Console.WriteLine(item);
}

А заполнение списка прервать нельзя. В результате получим ошибку Out of memory.

3)    Возможность корректировать значения коллекции после выполнения итератора. Так как yield return возвращает элемент коллекции на момент реальной обработки (при отображении значения элемента в консоли, например), то мы можем изменять элементы коллекции даже после выполнения итератора. Ведь итератор на самом деле не возвращает реальные значения, когда вы его вызываете. Итератор знает где взять значения. И он их вернет только тогда, когда они реально потребуются.  Это так называемая Lazy load.

1
2
3
4
5
6
7
8
9
10
11
12
13
IEnumerable<int> MultipleYieldReturn(IEnumerable<int> mass)
{
 foreach (var item in mass)
 yield return item * item;
} 
 
IEnumerable<int> MultipleLoop(IEnumerable<int> mass)
{
 var list = new List<int>();
 foreach (var item in mass)
 list.Add(item * item);
 return list;
}

А теперь вызовем эти методы:

1
2
3
4
5
6
7
 var mass = new List<int>() { 1, 2, 3 };
 var MultipleYieldReturn = Helper.MultipleYieldReturn(mass);
 var MultipleLoop = Helper.MultipleLoop(mass);
 
 mass.Add(4);
 Console.WriteLine(string.Join(",",MultipleYieldReturn));
 Console.WriteLine(string.Join(",", MultipleLoop));

Результат ожидаем:

 

После инициализации переменных MultipleYieldReturn и MultipleLoop добавим в коллекцию еще один элемент:

1
2
 Console.WriteLine(string.Join(",",MultipleYieldReturn));
 Console.WriteLine(string.Join(",", MultipleLoop));

Посмотрите на результат теперь:

 

На момент вывода результатов в консоль коллекция содержала значение 4. Так как yield return выдает значения в момент их запроса, итератор обработал все актуальные значения. Традиционный цикл выполнился при инициализации переменной MultipleLoop, а на тот момент коллекция содержала всего 3 значения.

 4) Обработка исключений с yield return имеет нюансы. Оператор yield return нельзя использовать в секции try-catch, только try-finally.

Например, как бы мы стали писать, не зная об ограничении:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public IEnumerable TransformData(List<string> data)
{
 foreach (string item in data)
 {
 try
 {
 yield return PrepareDataRow(item);
 }
 catch (Exception ex)
 {
 Console.Error.WriteLine(ex.Message);
 }
 }
}

В таком варианте блок catch никогда не отловит ошибку. Все дело в отложенном выполнении yield return. Об ошибке мы узнаем только в момент реальной работы с данными от итератора. Например, когда выводим данные из итератора на консоль.До тех пор итератор не работает с реальными данными.

Если вам все же нужно «отловить» ошибку в этом итераторе, то можно поступить так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public IEnumerable TransformData(List<string> data)
{
 string text;
 foreach (string item in data)
 {
 try
 {
 text = PrepareDataRow(item);
 }
 catch (Exception ex)
 {
 Console.Error.WriteLine(ex.Message);
 continue;
 }
 yield return text;
 }
}

Говоря о yield return нельзя не упомянуть о втором операторе с yield. Это yield break. По своему назначению он аналогичен оператору break, просто применяется только в итераторах. Вот небольшой пример:

1
2
3
4
5
6
7
8
9
10
IEnumerable<int> GetNumbers()
{
int i = 0;
while (true)
{
if (i = 5)
yield break;
yield return i++;
}
}

Из примера видно, что по достижении значения 5 итератор завершится, но до тех пор будет исправно выдавать значения.

Давайте подведем итоги. Когда же нужно использовать yield return?

  • При перечислении объектов. Итератор будет работать быстрее, чем возвращаемая коллекция. Да и накладные расходы памяти ниже;
  • В бесконечных циклах. Используя метод Take() вы всегда можете ограничить выборку.
Похожие статьи:

назад