yield
Ключевое слово yield используется в PHP для создания функции-генератора, которая может приостанавливаться и возобновляться во время выполнения.
Введение
Ключевое слово yield превращает обычную PHP-функцию в генератор — функцию, которая производит последовательность значений по одному за раз, приостанавливаясь после каждого значения и возобновляясь с того места, где остановилась, когда запрашивается следующее значение. На этой странице рассматривается, как работает yield, как возвращать ключи, как передавать значения обратно в генератор, синтаксис делегирования yield from, а также ситуации, когда генераторы экономят память по сравнению с созданием полного массива.
Функция становится генератором, как только содержит хотя бы один yield. Вызов такой функции не выполняет её тело — он возвращает объект Generator. Тело выполняется только по мере итерации, продвигаясь к следующему yield на каждом шаге. Поскольку генератор реализует интерфейс Iterator, его обычно обходят с помощью цикла foreach.
Первый генератор
Каждый yield передаёт одно значение вызывающему коду и замораживает функцию до тех пор, пока не будет запрошено следующее значение.
myGenerator() возвращает объект генератора вместо немедленного выполнения. Цикл foreach извлекает из него значения: на каждом проходе функция выполняется до следующего yield, выдаёт это значение и приостанавливается. Вывод:
Hello World !В отличие от функции, которая строит и возвращает массив через return, все три строки никогда не хранятся в памяти одновременно — каждая производится по требованию. Сравните это с тем, как обычная функция использует return для возврата одного значения и завершения работы.
Возврат ключей через yield
Генератор может возвращать пары ключ/значение с помощью синтаксиса yield $key => $value, подобно ассоциативному массиву. Ключи доступны внутри foreach:
<?php
function settings()
{
yield "host" => "localhost";
yield "port" => 5432;
yield "user" => "admin";
}
foreach (settings() as $key => $value) {
echo "$key = $value\n";
}Вывод:
host = localhost
port = 5432
user = adminЕсли ключи не указаны, PHP автоматически нумерует возвращаемые значения начиная с 0, как в индексированном массиве.
Генераторы ленивы: экономичный диапазон чисел
Главное преимущество yield заключается в том, что значения вычисляются только по мере необходимости. Генератор, производящий миллион чисел, использует постоянный объём памяти, тогда как массив из миллиона чисел выделяет память для всех сразу.
<?php
function gen_range($start, $end)
{
for ($i = $start; $i <= $end; $i++) {
yield $i;
}
}
$sum = 0;
foreach (gen_range(1, 1000000) as $n) {
$sum += $n;
}
echo $sum;Вывод:
500000500000Цикл никогда не создаёт массив [1, 2, …, 1000000]; в каждый момент времени существует только одно целое число. Это шаблон, к которому стоит прибегать при построчном чтении больших файлов или потоковой передаче строк из базы данных.
Возврат значения из генератора
Генератор также может использовать return для передачи финального значения (PHP 7+). Возвращаемое значение не является частью итерации — его считывают после завершения с помощью getReturn():
<?php
function counter()
{
yield 1;
yield 2;
return "done";
}
$gen = counter();
foreach ($gen as $value) {
echo $value . " ";
}
echo $gen->getReturn();Вывод:
1 2 doneВызов getReturn() до завершения итерации по генератору выбрасывает исключение, поэтому всегда сначала исчерпайте его полностью.
Передача значений с помощью send()
Генераторы работают в обоих направлениях: выражение yield также может получать значение, отправленное вызывающим кодом через send(). Это основа корутин.
<?php
function echoTimes()
{
while (true) {
$received = yield;
echo "Got: $received\n";
}
}
$gen = echoTimes();
$gen->current(); // prime the generator (run up to the first yield)
$gen->send("a");
$gen->send("b");Вывод:
Got: a
Got: bsend($value) возобновляет работу генератора, заставляя приостановленное выражение yield вычислиться в $value, после чего выполнение продолжается до следующего yield.
Делегирование с помощью yield from
yield from (PHP 7+) возвращает каждое значение из другого генератора, массива или Traversable, встраивая его в текущий генератор без ручного цикла:
<?php
function inner()
{
yield 2;
yield 3;
}
function outer()
{
yield 1;
yield from inner();
yield from [4, 5];
}
foreach (outer() as $value) {
echo $value . " ";
}Вывод:
1 2 3 4 5 Когда использовать генератор
- Большие или неограниченные последовательности — чтение многогигабайтного файла или бесконечного потока, когда хранить всё в массиве невозможно.
- Дорогостоящие значения, которые могут не понадобиться — если потребитель может остановиться досрочно (
break), генератор избавит от вычисления остального. - Более чистый код итерации — замена пользовательского класса
Iteratorодной функцией.
Избегайте генераторов, когда нужен произвольный доступ ($arr[42]), дешёвый вызов count() или многократная итерация по одним и тем же данным — генератор можно обойти только один раз. В таких случаях используйте обычный массив, возможно создав его с помощью PHP-функций, таких как array_map.