День девяносто первый. #ЗаметкиНаПолях

Выражение yield return. Особенности работы

1. Множественные итерации

Побочным эффектом выражения yield return является то, что множественные вызовы приводят к множественным итерациям:

static void Main(string[] args)

{

var invoices = GetInvoices();

DoubleAmounts(invoices);

Console.WriteLine(invoices.First().Amount);

}



class Invoice { public double Amount { get; set; } }

static IEnumerable<Invoice> GetInvoices()

{

for (var i = 1; i < 11; i++)

yield return new Invoice { Amount = i * 10 };

}

static void DoubleAmounts(IEnumerable<Invoice> invoices)

{

foreach (var invoice in invoices)

invoice.Amount = invoice.Amount * 2;

}

Попробуйте догадаться, что будет выведено в консоль? Несмотря на то, что интуиция подсказывает результат 20, выведено будет 10:

1. Когда выполняется строка var invoices = GetInvoices(); мы не получаем список счетов (Invoice), мы получаем итератор, создающий объекты Invoice.

2. Этот итератор передаётся в метод DoubleAmounts.

3. Внутри метода DoubleAmounts итератор используется для создания объектов Invoice и удвоения суммы счёта (свойство Amount) каждого объекта.

4. Однако все объекты, созданные в методе DoubleAmounts, выбрасываются, поскольку на них нет ссылок из внешнего кода.

5. Когда мы возвращаемся во внешний код, у нас остаётся только ссылка на итератор. Вызывая метод First, мы снова просим создать объект Invoice. Это новый объект, поэтому в результате его свойство Amount будет иметь значение 10.

Поскольку это неочевидное поведение, инструменты вроде Resharper предупреждают вас о множественных итерациях.

2. Отложенное выполнение

Все примеры использования yield return имеют общее свойство. Код выполняется только тогда, когда это необходимо. Механизм паузы/возобновления в методе-итераторе делает это возможным. Используя отложенное выполнение, мы можем делать методы проще, иногда быстрее, а иногда и в принципе делать возможным то, что было невозможно ранее. Например, можно сделать бесконечный генератор чисел и выбирать из него только нужное количество. Весь модуль LINQ в C# построен вокруг отложенного исполнения. Вот как оно повышает эффективность кода:

var dollarPrices = FetchProducts().Take(10)

.Select(p => p.CalculatePrice())

.OrderBy(price => price)

.Take(5)

.Select(price => ConvertTo$(price));

Допустим, у нас 1000 продуктов. Без отложенного выполнения, методу бы пришлось:

1. Выбрать все 1000 продуктов

2. Посчитать цену всех 1000 продуктов

3. Выстроить все продукты по возрастанию цены

4. Перевести все цены в доллары

5. Выбрать 5 продуктов с самой высокой ценой

Используя отложенное выполнение, код:

1. Выбирает 10 продуктов

2. Вычисляет цену 10 продуктов

3. Выстраивает из по возрастанию цены

4. Отбирает первые 5 и переводит их цены в доллары

Несмотря на то, что это выдуманный пример, он чётко показывает, как отложенное выполнение может сильно повысить эффективность. Хотя, заметьте, что пример с отложенным выполнением может делать не то, что вы хотите. В примере выше из всех продуктов отбираются не 10 с наиболее высокой ценой, а 10 первых попавшихся. Поэтому нужно быть внимательным в порядке вызова методов LINQ.



Источники:

-
https://docs.microsoft.com/ru-ru/dotnet/csharp/language-reference/keywords/yield

-
https://www.kenneth-truyers.net/2016/05/12/yield-return-in-c/