Поскольку мой друг спросил меня о ключевом слове yield, я задумался о некоторых примерах, в которых его использование имеет решающее значение для повышения производительности и экономии времени. Итак, я решил их записать.

Что такое итератор?

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

В C # мы можем сказать, что итератор - это просто каждый класс, расширяющий интерфейс IEnumerable. Этот интерфейс содержит метод GetEnumerator, который используется компилятором для преобразования foreach циклов.

Для целей этой статьи вам не нужно беспокоиться о внутренней части реализации IEnumerator (тип интерфейса, возвращаемый методом GetEnumerator). Более подробное объяснение итераторов в C # можно найти в Microsoft Docs.

Что означает ключевое слово yield?

Ключевое слово yield используется для создания итератора особого типа, в котором элементы генерируются по запросу.

Например, возьмем простой Main метод, который выполняет итерацию по списку элементов, предоставленных методом GetElements.

Вывод:

Building element #1...
Consuming element #1...
Building element #2...
Consuming element #2...
Building element #3...
Consuming element #3...

Хотите прочитать эту историю позже? Сохранить в журнале.

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

Будьте осторожны с несколькими поколениями!

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

Давайте посмотрим на код (рассмотрим тот же GetElements, что и раньше)…

Вывод:

Building element #1...
Consuming element #1...
Building element #2...
Consuming element #2...
Building element #3...
Consuming element #3...
--- Some additional code ---
Building element #1...
Consuming element #1...
Building element #2...
Consuming element #2...
Building element #3...
Consuming element #3...

Как видим, каждый элемент был построен дважды. В этом случае вы можете добавить ToArray() в конце строки 3, чтобы избежать нескольких поколений.

Перерыв в доходности

Ключевое слово break позволяет нам остановить генерацию элементов. Чтобы увидеть, как это работает, давайте добавим к нашему методу флаг. Этот флаг будет указывать на то, что мы хотим, чтобы был возвращен только один элемент, просто игнорируя другие.

Вывод:

Building element #1...
Consuming element #1...

Примеры

Потребление подмножества тяжелых элементов

Самый простой пример использования yield - это случай, когда вам не нужно перебирать все элементы, чтобы получить то, что вы хотите.

Итак, представьте, что у вас есть класс с тяжелой конструкцией. Это может зависеть, например, от внешнего API или базы данных.

В нашем примере он будет представлен следующим классом

Теперь представьте, что у вас есть некий GetElements метод, который возвращает 10 экземпляров HeavyObject, но по какой-то причине вы просто хотите использовать первые 3 элемента.

В реальном приложении метод GetElements может извлекать огромный сериализованный массив объектов из внешнего API, а тяжелая конструкция может заключаться в их десериализации. А потребление только нескольких элементов могло быть связано с разбиением на страницы или фильтрацией.

Если мы реализуем метод с использованием ключевого слова yield, будут построены только первые 3 элемента.

И на выходе будет

Building element #1...
Consuming element #1...
Building element #2...
Consuming element #2...
Building element #3...
Consuming element #3...

С другой стороны, если бы мы использовали список для реализации метода,

Тогда выход для того же цикла был бы

Building element #1...
Building element #2...
Building element #3...
Building element #4...
Building element #5...
Building element #6...
Building element #7...
Building element #8...
Building element #9...
Building element #10...
Consuming element #1...
Consuming element #2...
Consuming element #3...

Обратите внимание, что все объекты были построены до их использования. И даже неиспользованные были построены! Также обратите внимание, что в первом случае (с использованием yield) не было создано ни одного списка или массива для возврата элементов, что позволяет сэкономить выделение памяти при выполнении приложения.

Составление итераторов

Другой замечательный пример использования yield - возможность составления итераторов. Чтобы лучше понять этот случай, давайте создадим базовую реализацию метода расширения where<T> для IEnumerable<T>, предоставляемого библиотекой LINQ на C #.

Исходную реализацию можно найти здесь.

Теперь рассмотрим следующий код, в котором мы дважды фильтруем массив, чтобы получить первое число от 5 до 8.

Результат конечно 6. Но здесь нас интересует, сколько предикатов выполняется. Вот почему я разделил предикаты на методы, которые отображают сравнение на консоли.

И на выходе

1 is less than or equal to 5.
2 is less than or equal to 5.
3 is less than or equal to 5.
4 is less than or equal to 5.
5 is less than or equal to 5.
6 is greater than 5.
6 is less than 8.
The result is 6.

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

И вы можете догадаться, что произошло бы, если бы мы использовали список для реализации метода where: первый вызов (ln 6) вернет список [6, 7, 8, 9, 10], второй (ln 7) вернет список [6, 7] и, наконец, мы достигнет числа 6.

Также обратите внимание, что если мы перевернем строки 6 и 7, результат будет

1 is less than 8.
1 is less than or equal to 5.
2 is less than 8.
2 is less than or equal to 5.
3 is less than 8.
3 is less than or equal to 5.
4 is less than 8.
4 is less than or equal to 5.
5 is less than 8.
5 is less than or equal to 5.
6 is less than 8.
6 is greater than 5.
The result is 6.

Таким образом, в этом случае не только использование ключевого слова yield при реализации, но и порядок вызовов повлияли на производительность.

Вот и все, ребята!

Если вы нашли эту статью полезной, используйте кнопку хлопайте в ладоши (👏) и не стесняйтесь спрашивать или комментировать. Приветствуется и конструктивная критика :)