Skip to content

生成器委托

概述

生成器委托(Generator Delegation)通过 yield from 关键字实现,是 PHP 7.0 引入的特性。yield from 允许一个生成器将产出值的职责委托给另一个生成器、数组或任何 Traversable 对象。这极大简化了嵌套生成器的编写,支持递归生成器模式,并能获取子生成器的返回值。

PHP 版本

yield from 从 PHP 7.0 开始可用。本文基于 PHP 8.1+ 编写。

基础概念

什么是生成器委托

生成器委托是指一个生成器将"产出值"的工作交给另一个可迭代对象来完成。外层生成器不需要手动遍历内层生成器并逐个 yield,只需使用 yield from 即可。

yield from 的可委托对象

yield from 可以委托给以下类型的对象:

  • 生成器(Generator)
  • 数组(Array)
  • 任何实现了 Traversable 接口的对象(包括迭代器)

委托的特性

yield from 不仅是简单的转发,它还支持:

  • 键和值的双向传递
  • send() 值的透传
  • 子生成器 return 值的捕获

语法与代码

基本委托

php
<?php
declare(strict_types=1);

function innerGen(): \Generator
{
    yield 'a';
    yield 'b';
    yield 'c';
}

function outerGen(): \Generator
{
    yield 'start';
    yield from innerGen(); // 委托给 innerGen
    yield 'end';
}

foreach (outerGen() as $value) {
    echo "{$value} ";
}
// 输出: start a b c end

委托数组

php
<?php
declare(strict_types=1);

function fromArray(): \Generator
{
    yield from ['x', 'y', 'z'];
}

foreach (fromArray() as $value) {
    echo "{$value} ";
}
// 输出: x y z

委托迭代器

php
<?php
declare(strict_types=1);

function fromIterator(): \Generator
{
    $arrayObj = new \ArrayObject(['one', 'two', 'three']);
    yield from $arrayObj->getIterator();
}

foreach (fromIterator() as $value) {
    echo "{$value} ";
}
// 输出: one two three

委托多个生成器

php
<?php
declare(strict_types=1);

function evens(int $max): \Generator
{
    for ($i = 2; $i <= $max; $i += 2) {
        yield $i;
    }
}

function odds(int $max): \Generator
{
    for ($i = 1; $i <= $max; $i += 2) {
        yield $i;
    }
}

function allNumbers(int $max): \Generator
{
    yield from evens($max);
    yield from odds($max);
}

foreach (allNumbers(10) as $num) {
    echo "{$num} ";
}
// 输出: 2 4 6 8 10 1 3 5 7 9

捕获子生成器的返回值

php
<?php
declare(strict_types=1);

function calculateSum(array $nums): \Generator
{
    $sum = 0;
    foreach ($nums as $n) {
        yield $n;
        $sum += $n;
    }
    return $sum;
}

function processWithResult(array $data): \Generator
{
    yield 'processing...';

    // yield from 表达式返回子生成器的 return 值
    $total = yield from calculateSum($data);

    yield "total: {$total}";
}

foreach (processWithResult([10, 20, 30]) as $value) {
    echo "{$value}\n";
}
// processing...
// 10
// 20
// 30
// total: 60

详细说明

send() 值的透传

yield from 不仅仅透传产出值,还会将调用者的 send() 值传递给子生成器中当前的 yield 表达式。

php
<?php
declare(strict_types=1);

function subGenerator(): \Generator
{
    $received = yield 'sub start';
    yield "sub received: {$received}";
    return 'sub done';
}

function mainGenerator(): \Generator
{
    yield 'main start';
    $result = yield from subGenerator();
    yield "main got: {$result}";
}

$gen = mainGenerator();
echo $gen->current() . "\n";    // main start
$gen->next();
echo $gen->current() . "\n";    // sub start
echo $gen->send('hello') . "\n"; // main got: sub done
// send('hello') 传给子生成器的 yield,子生成器产出 "sub received: hello"
// 然后子生成器 return 'sub done',被 yield from 捕获
// mainGenerator 继续执行 yield "main got: sub done"

异常透传

通过 Generator::throw() 抛出的异常也会透传到子生成器中。

php
<?php
declare(strict_types=1);

function delegatingGenerator(): \Generator
{
    try {
        $result = yield from innerGenerator();
        yield "result: {$result}";
    } catch (\RuntimeException $e) {
        yield "caught in outer: {$e->getMessage()}";
    }
}

function innerGenerator(): \Generator
{
    yield 'inner 1';
    yield 'inner 2';
    return 'inner done';
}

$gen = delegatingGenerator();
echo $gen->current() . "\n"; // inner 1
$gen->next();
echo $gen->current() . "\n"; // inner 2
echo $gen->throw(new \RuntimeException('error'));
// caught in outer: error

键的透传

yield from 会保留子生成器产出的键值对关系。

php
<?php
declare(strict_types=1);

function namedGenerator(): \Generator
{
    yield 'name' => 'Alice';
    yield 'age' => 30;
    yield 'city' => 'Beijing';
}

function wrapperGenerator(): \Generator
{
    yield from namedGenerator();
}

foreach (wrapperGenerator() as $key => $value) {
    echo "{$key} => {$value}\n";
}
// name => Alice
// age => 30
// city => Beijing

实战示例

实战:递归生成器(树遍历)

php
<?php
declare(strict_types=1);

class TreeNode
{
    public function __construct(
        public string $value,
        /** @var array<int, TreeNode> */
        public array $children = []
    ) {}
}

function traverseTree(TreeNode $node): \Generator
{
    yield $node->value;

    foreach ($node->children as $child) {
        yield from traverseTree($child); // 递归委托
    }
}

$tree = new TreeNode('root', [
    new TreeNode('child1', [
        new TreeNode('grandchild1'),
        new TreeNode('grandchild2'),
    ]),
    new TreeNode('child2', [
        new TreeNode('grandchild3'),
    ]),
]);

foreach (traverseTree($tree) as $value) {
    echo "{$value}\n";
}
// root
// child1
// grandchild1
// grandchild2
// child2
// grandchild3

实战:目录递归遍历

php
<?php
declare(strict_types=1);

function listFiles(string $directory, string $pattern = '*'): \Generator
{
    $items = glob("{$directory}/{$pattern}");

    foreach ($items as $item) {
        if (is_dir($item)) {
            yield from listFiles($item, $pattern); // 递归委托子目录
        } else {
            yield $item;
        }
    }
}

foreach (listFiles('/path/to/project', '*.php') as $file) {
    echo $file . "\n";
}

实战:任务委派系统

php
<?php
declare(strict_types=1);

interface TaskHandler
{
    public function handle(array $payload): \Generator;
}

class EmailTaskHandler implements TaskHandler
{
    public function handle(array $payload): \Generator
    {
        foreach ($payload['recipients'] as $recipient) {
            yield "发送邮件给: {$recipient}";
        }
        return count($payload['recipients']);
    }
}

class LogTaskHandler implements TaskHandler
{
    public function handle(array $payload): \Generator
    {
        yield "记录日志: {$payload['message']}";
        return 1;
    }
}

class TaskDispatcher
{
    /** @var array<string, TaskHandler> */
    private array $handlers = [];

    public function register(string $type, TaskHandler $handler): void
    {
        $this->handlers[$type] = $handler;
    }

    public function dispatch(array $tasks): \Generator
    {
        $totalResults = [];

        foreach ($tasks as $task) {
            $type = $task['type'];
            if (isset($this->handlers[$type])) {
                $result = yield from $this->handlers[$type]->handle($task);
                $totalResults[$type] = $result;
            }
        }

        return $totalResults;
    }
}

$dispatcher = new TaskDispatcher();
$dispatcher->register('email', new EmailTaskHandler());
$dispatcher->register('log', new LogTaskHandler());

$tasks = [
    ['type' => 'email', 'recipients' => ['a@test.com', 'b@test.com']],
    ['type' => 'log', 'message' => '系统启动'],
];

$gen = $dispatcher->dispatch($tasks);
foreach ($gen as $output) {
    echo "{$output}\n";
}
// 发送邮件给: a@test.com
// 发送邮件给: b@test.com
// 记录日志: 系统启动

print_r($gen->getReturn());
// Array ( [email] => 2 [log] => 1 )

注意事项

委托生成器的键冲突

当委托多个生成器时,如果不同生成器产出了相同的键,后面的键会覆盖前面的。

php
<?php
declare(strict_types=1);

function genA(): \Generator
{
    yield 0 => 'a0';
    yield 1 => 'a1';
}

function genB(): \Generator
{
    yield 0 => 'b0'; // 键 0 与 genA 冲突
    yield 2 => 'b2';
}

function combined(): \Generator
{
    yield from genA();
    yield from genB();
}

foreach (combined() as $key => $value) {
    echo "{$key}: {$value}\n";
}
// 0: a0
// 1: a1
// 0: b0(键 0 重复)
// 2: b2

yield from 与 foreach 的区别

php
<?php
declare(strict_types=1);

// 方式一:foreach 手动 yield(不支持 send 透传和 return 捕获)
function manualYield(): \Generator
{
    foreach (innerGen() as $value) {
        yield $value; // send() 值不会传给 innerGen
    }
}

// 方式二:yield from(支持 send 透传和 return 捕获)
function delegatedYield(): \Generator
{
    return yield from innerGen();
}

嵌套深度限制

虽然 yield from 支持任意深度的嵌套,但过深的嵌套可能导致栈溢出或调试困难。建议将嵌套深度控制在合理范围内。

最佳实践

  1. 用 yield from 替代手动委托:需要转发另一个生成器的值时,优先使用 yield from
  2. 利用返回值进行聚合:通过 yield from 表达式捕获子生成器的返回值,用于聚合统计。
  3. 递归生成器使用 yield from:树/目录遍历等递归场景中,yield from 是最自然的表达方式。
  4. 注意键的唯一性:委托多个生成器时注意键的冲突问题。
  5. 异常处理在委托层:在 yield from 外层使用 try/catch 捕获子生成器的异常。
php
<?php
declare(strict_types=1);

/**
 * 扁平化嵌套数组。
 *
 * @param iterable<mixed> $nested
 * @return \Generator<int, mixed>
 */
function flatten(iterable $nested): \Generator
{
    foreach ($nested as $item) {
        if (is_iterable($item) && !is_string($item)) {
            yield from flatten($item);
        } else {
            yield $item;
        }
    }
}

$nested = [1, [2, [3, 4]], [5, [6, [7, 8]]]];
foreach (flatten($nested) as $value) {
    echo "{$value} ";
}
// 输出: 1 2 3 4 5 6 7 8

参考链接