Поскольку мой друг спросил меня о ключевом слове 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
при реализации, но и порядок вызовов повлияли на производительность.
Вот и все, ребята!
Если вы нашли эту статью полезной, используйте кнопку хлопайте в ладоши (👏) и не стесняйтесь спрашивать или комментировать. Приветствуется и конструктивная критика :)