生成器委托
概述
生成器委托(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: b2yield 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 支持任意深度的嵌套,但过深的嵌套可能导致栈溢出或调试困难。建议将嵌套深度控制在合理范围内。
最佳实践
- 用 yield from 替代手动委托:需要转发另一个生成器的值时,优先使用
yield from。 - 利用返回值进行聚合:通过
yield from表达式捕获子生成器的返回值,用于聚合统计。 - 递归生成器使用 yield from:树/目录遍历等递归场景中,
yield from是最自然的表达方式。 - 注意键的唯一性:委托多个生成器时注意键的冲突问题。
- 异常处理在委托层:在
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