Menu

Генераторы PHP: yield, yield from и экономия памяти

Генератор это функция, которая через yield отдаёт значения по одному, поэтому foreach может пройти миллион элементов, не создавая массив. yield, ключи, экономия памяти, бесконечные последовательности, yield from, send() и getReturn().

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

Генератор это функция, в которой есть yield. Её вызов не выполняет тело; он возвращает объект Generator, который можно обойти через foreach. Каждый yield $value отдаёт одно значение циклу и приостанавливает функцию, пока цикл не попросит следующее, поэтому значения создаются по одному, а не собираются сначала в массив.

Вывод чередуется между генератором и циклом: каждое значение создаётся только тогда, когда цикл его просит.

Генераторы экономят память

range(1, 1000000) строит массив из миллиона целых чисел ещё до начала цикла. Генератор, который выдаёт те же числа, никогда не держит больше одного. Запустите и сравните, сколько памяти нужно каждому варианту:

Оба цикла печатают одну и ту же сумму, 500000500000, но вариант с массивом сообщает extra memory 16.0 MB, а генератор extra memory 0.0 MB. Разрыв растёт вместе с данными. Та же идея делает генераторы полезными для больших файлов, результатов запросов к базе данных и страниц API: обработать один элемент, выбросить его, взять следующий.

Ключи и значения в yield

yield $key => $value задаёт ключ, который получает foreach ($gen as $key => $value). Без ключа генератор нумерует значения с 0, как список. Частое применение это чтение файла построчно с номерами строк:

В памяти одновременно только одна строка, поэтому та же функция справится и с лог-файлом на 2 ГБ. Используемые здесь функции для работы с файлами описаны в fopen() и fgets().

Бесконечные последовательности и ленивые конвейеры

Генератор может работать бесконечно, потому что выполняется ровно настолько, насколько просит потребитель. Остановитесь через break или оберните его в другой генератор, который берёт первые несколько значений. Цепочка небольших генераторов даёт конвейер, где каждый шаг ленивый:

fibonacci() сам по себе никогда не заканчивается, но программа завершается: take() перестаёт просить значения после нужного количества, и ничего сверх этого не вычисляется.

yield from: передача работы другому генератору

yield from пропускает через себя каждое значение массива, итератора или другого генератора. Так генератор разбивают на более мелкие или рекурсивно обходят вложенную структуру:

yield from сохраняет внутренние ключи, поэтому оба массива выдают ключи 0 и 1. Затем iterator_to_array() перезаписывает a и b. Передавайте false вторым аргументом всякий раз, когда важны только значения.

send() и getReturn()

Генератор умеет и получать значения. $x = yield; приостанавливается и ждёт; $gen->send($value) возобновляет генератор, и результатом этого yield становится $value. А ещё генератор может через return вернуть итоговое значение, которое читают через getReturn() после его завершения.

Большинству кода send() не нужен; повседневное применение это ленивое чтение через foreach. На send() построены библиотеки корутин.

Генератор можно обойти только один раз

Генераторы работают только вперёд и не перематываются. Когда foreach исчерпал генератор, второй цикл по тому же объекту выбрасывает исключение:

Решение: вызывать функцию-генератор заново для каждого прохода, foreach (colors() as $c), или один раз собрать значения через iterator_to_array(colors(), false), когда они действительно нужны дважды. Если вы делаете так часто, для этих данных правильным выбором был обычный массив.

Часто задаваемые вопросы

Что делает yield в PHP?

yield $value отдаёт одно значение коду, который обходит функцию в цикле, и приостанавливает функцию прямо в этом месте. Следующая итерация цикла возобновляет её после yield. Любая функция, содержащая yield, при вызове не выполняется, а возвращает объект Generator.

Зачем использовать генератор вместо массива?

Массив держит в памяти все элементы сразу; генератор выдаёт по одному элементу. Обход range(1, 1000000) выделяет память под все миллион целых чисел, а генератор, который их выдаёт, держит расход памяти постоянным. Генераторы ещё и позволяют бесконечные последовательности и прекращают работу, как только цикл прерывается.

Что такое yield from в PHP?

yield from $source передаёт дальше каждое значение другого генератора, массива или Traversable, как будто текущий генератор выдал их сам. Кроме того, это выражение равно значению, которое вернул внутренний генератор.

Можно ли обойти генератор дважды?

Нет. Генератор работает только вперёд. После того как foreach закончился, повторный обход выбрасывает Exception: Cannot traverse an already closed generator. Вызовите функцию-генератор снова, чтобы получить новый генератор, или сохраните значения через iterator_to_array(), если они нужны дважды.

Как превратить генератор в массив?

iterator_to_array($gen) собирает все значения, сохраняя ключи генератора. Если ключи повторяются, например после yield from, более поздние значения перезаписывают более ранние, поэтому передайте false вторым аргументом, чтобы получить простой список: iterator_to_array($gen, false).

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ