HardТеория4 min

Генераторы

yield, yield from, Generator class, делегирование, практические примеры

Что такое генератор

Генератор -- функция с ключевым словом 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+)

Вопросы с экзамена ZCE

Проверь себя

5 из 15

Что делает `yield from`?

Что возвращает вызов функции-генератора?

Что произойдёт при `yield from [1, 2, 3]`?

Что делает метод send()?

Что произойдёт при вызове rewind() на уже начатом генераторе?