Что такое генератор
Генератор -- функция с ключевым словом yield, которая приостанавливает выполнение после каждой выдачи значения и возобновляется при запросе следующего. Это ленивое вычисление с потреблением памяти O(1) вместо O(n).
При вызове функция-генератор не выполняется, а возвращает объект Generator (реализует Iterator).
<?php
declare(strict_types=1);
// Regular function — all values in memory
function rangeArray(int $start, int $end): array
{
$result = [];
for ($i = $start; $i <= $end; $i++) { $result[] = $i; }
return $result;
}
// Generator — one value at a time, O(1) memory
function rangeGenerator(int $start, int $end): Generator
{
for ($i = $start; $i <= $end; $i++) {
yield $i; // Pauses here, resumes on next iteration
}
}
foreach (rangeGenerator(1, 1_000_000) as $number) {
if ($number > 5) break;
}
Классический пример -- бесконечная последовательность Фибоначчи:
<?php
declare(strict_types=1);
function fibonacci(): Generator
{
$a = 0;
$b = 1;
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
foreach (fibonacci() as $i => $number) {
if ($i >= 10) break;
echo "$number ";
}
// 0 1 1 2 3 5 8 13 21 34
Синтаксис yield
<?php
declare(strict_types=1);
function yieldExamples(): Generator
{
// yield value — auto key (0, 1, 2...)
yield 'red';
yield 'green';
// yield key => value — explicit key
yield 'role' => 'admin';
// yield without value — yields null
yield;
}
function priorityExample(): Generator
{
// yield has very LOW precedence — use parentheses in expressions
$data = (yield 'request');
// Without parens: yield ('value' . $data) — NOT what we want
echo (yield 'value') . ' suffix';
}
Отправка значений в генератор (send)
Метод send() передаёт значение внутрь генератора -- оно становится результатом выражения yield. Одновременно генератор продвигается до следующего yield:
<?php
declare(strict_types=1);
function accumulator(): Generator
{
$total = 0;
while (true) {
$value = yield $total;
$total += $value;
}
}
$gen = accumulator();
echo $gen->current() . "\n"; // 0 (initial yield)
echo $gen->send(5) . "\n"; // 5 (0 + 5)
echo $gen->send(3) . "\n"; // 8 (5 + 3)
echo $gen->send(10) . "\n"; // 18 (8 + 10)
Правила: current() или первый send() запускает генератор до первого yield. Значение send() становится результатом текущего yield.
Бросание исключений (throw)
Метод throw() бросает исключение внутрь генератора в точке текущего yield. Генератор может перехватить его через try/catch:
<?php
declare(strict_types=1);
function resilientWorker(): Generator
{
while (true) {
try {
$task = yield;
echo "Processed: $task\n";
} catch (\RuntimeException $e) {
echo "Error: {$e->getMessage()}\n";
}
}
}
$worker = resilientWorker();
$worker->current();
$worker->send('task1'); // Processed: task1
$worker->throw(new \RuntimeException('Network fail')); // Error: Network fail
$worker->send('task2'); // Processed: task2
Делегирование (yield from)
yield from передаёт управление другому генератору, массиву или Traversable. Возвращаемое значение внутреннего генератора становится результатом yield from:
<?php
declare(strict_types=1);
function inner(): Generator
{
yield 1;
yield 2;
return 'inner result';
}
function outer(): Generator
{
$result = yield from inner(); // Yields 1, then 2
echo "Inner returned: $result\n";
yield 3;
}
// Also works with arrays and any iterable
function combined(): Generator
{
yield from [10, 20];
yield from new ArrayIterator([30, 40]);
}
Возвращаемые значения (return + getReturn)
Значение return НЕ выдаётся через yield -- доступно только через getReturn() после завершения генератора:
<?php
declare(strict_types=1);
function parseFile(string $path): Generator
{
$handle = fopen($path, 'r');
$count = 0;
while (($line = fgets($handle)) !== false) {
yield trim($line);
$count++;
}
fclose($handle);
return $count;
}
$gen = parseFile('/tmp/data.txt');
foreach ($gen as $line) { /* process */ }
echo "Lines: " . $gen->getReturn(); // Available only after iteration
Важно: Вызов
getReturn()до завершения генератора бросит исключение.
Класс Generator -- все методы
| Метод | Описание |
|---|---|
current() |
Текущее yield-значение |
key() |
Текущий yield-ключ |
next() |
Продвинуть до следующего yield |
rewind() |
Перемотка (только до первого yield!) |
send(mixed $value) |
Отправить значение и продвинуть |
throw(Throwable $e) |
Бросить исключение в точке yield |
valid() |
Генератор ещё не завершён? |
getReturn() |
Значение return (после завершения) |
<?php
declare(strict_types=1);
function demo(): Generator
{
yield 'a' => 1;
yield 'b' => 2;
return 'done';
}
$gen = demo();
echo $gen->key() . ': ' . $gen->current() . "\n"; // a: 1
$gen->next();
echo $gen->key() . ': ' . $gen->current() . "\n"; // b: 2
$gen->next();
var_dump($gen->valid()); // false
echo $gen->getReturn(); // done
Генераторы vs Iterator
| Критерий | Generator | Iterator |
|---|---|---|
| Объём кода | Одна функция | Класс с 5 методами |
| Повторная итерация | Нет | Да (rewind) |
| Память | O(1) | Зависит от реализации |
| send()/throw() | Да | Нет |
| yield from | Да | Нет |
Правило: одноразовая ленивая итерация -- генератор. Повторная -- Iterator/IteratorAggregate.
Практические примеры
Ленивое чтение CSV
<?php
declare(strict_types=1);
function readCsv(string $path): Generator
{
$handle = fopen($path, 'r');
try {
$headers = fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
yield array_combine($headers, $row);
}
} finally {
fclose($handle);
}
}
Пагинация API
<?php
declare(strict_types=1);
function fetchAll(string $url): Generator
{
$page = 1;
do {
$data = json_decode(file_get_contents("{$url}?page={$page}"), true);
foreach ($data['items'] as $item) { yield $item; }
$page++;
} while ($data['has_next'] ?? false);
}
Pipeline обработки данных
<?php
declare(strict_types=1);
function readLines(string $path): Generator
{
$h = fopen($path, 'r');
while (($line = fgets($h)) !== false) { yield trim($line); }
fclose($h);
}
function filterNonEmpty(iterable $lines): Generator
{
foreach ($lines as $line) {
if ($line !== '') { yield $line; }
}
}
function addNumbers(iterable $lines): Generator
{
$i = 1;
foreach ($lines as $line) { yield sprintf('%04d: %s', $i++, $line); }
}
$pipeline = addNumbers(filterNonEmpty(readLines('/tmp/data.txt')));
Обход дерева
<?php
declare(strict_types=1);
function walkTree(array $node, int $depth = 0): Generator
{
yield ['name' => $node['name'], 'depth' => $depth];
foreach ($node['children'] ?? [] as $child) {
yield from walkTree($child, $depth + 1);
}
}
Типизация генераторов
<?php
declare(strict_types=1);
/**
* @return Generator<int, string, mixed, int>
* TKey=int, TValue=string, TSend=mixed, TReturn=int
*/
function words(string $text): Generator
{
$list = explode(' ', $text);
foreach ($list as $i => $word) { yield $i => $word; }
return count($list);
}
// Accept generators via iterable type
function sum(iterable $numbers): int
{
$total = 0;
foreach ($numbers as $n) { $total += $n; }
return $total;
}
Важные нюансы
- Нельзя перемотать после начала --
rewind()бросит исключение - Одноразовый -- повторный
foreachничего не выведет - O(1) память, но каждый
yieldимеет overhead -- для маленьких коллекций массив быстрее - Не корутины -- это полу-корутины, для полноценных используйте
Fiber(PHP 8.1+)