Генераторы
Обычная функция сначала собирает весь результат, а потом возвращает его целиком. Для миллиона строк это миллион элементов в памяти. Генератор отдаёт значения по одному — ровно тогда, когда их просят. Памяти нужно столько, сколько занимает один элемент.
yield
Функция со словом yield становится генератором. Каждый yield отдаёт одно значение и приостанавливает функцию до следующего запроса:
<?php
function numbers()
{
yield 1;
yield 2;
yield 3;
}
foreach (numbers() as $number) {
echo $number, ' ';
}Выведется 1 2 3. Генератор перебирают циклом foreach, как массив.
Значения по требованию
Код генератора выполняется не сразу, а по шагам — между итерациями цикла:
<?php
function steps()
{
echo '[начало] ';
yield 'A';
echo '[середина] ';
yield 'B';
echo '[конец]';
}
foreach (steps() as $step) {
echo $step, ' ';
}Выведется [начало] A [середина] B [конец]: функция продолжается с того места, где остановилась.
Генератор вместо массива
Диапазон чисел без создания массива:
<?php
function countTo(int $limit)
{
for ($i = 1; $i <= $limit; $i++) {
yield $i;
}
}
$sum = 0;
foreach (countTo(1000000) as $number) {
$sum += $number;
}
echo $sum; // 500000500000Массив из миллиона чисел занял бы десятки мегабайт. Генератор хранит только текущее значение.
Ключи
yield может отдавать пару «ключ — значение»:
<?php
function prices()
{
yield 'чай' => 250;
yield 'кофе' => 400;
}
foreach (prices() as $name => $price) {
echo $name, ': ', $price, "\n";
}Остановка по условию
Генератор может быть бесконечным — перебор прекращает тот, кто его использует:
<?php
function evenNumbers()
{
$number = 0;
while (true) {
$number += 2;
yield $number;
}
}
foreach (evenNumbers() as $number) {
if ($number > 10) {
break;
}
echo $number, ' ';
}Выведется 2 4 6 8 10.
Чтение большого файла
Главное применение — обработка данных, которые не помещаются в память: большие файлы, выборки из базы. Генератор читает файл построчно:
<?php
function readLines(string $path)
{
$handle = fopen($path, 'r');
while (($line = fgets($handle)) !== false) {
yield rtrim($line);
}
fclose($handle);
}
$file = sys_get_temp_dir() . '/lines-demo.txt';
file_put_contents($file, "первая\nвторая\nтретья\n");
foreach (readLines($file) as $number => $line) {
echo $number + 1, '. ', $line, "\n";
}Частая ошибка
Генератор проходят только один раз. После завершения цикла начать его заново нельзя — нужно вызвать функцию ещё раз. И к нему не применимы count() и обращение по индексу:
<?php
function letters()
{
yield 'a';
yield 'b';
}
$generator = letters();
echo implode(', ', iterator_to_array($generator)), "\n"; // a, b
echo count(iterator_to_array(letters())); // 2iterator_to_array() превращает генератор в массив — но тогда теряется экономия памяти.