Skip to content

生成器语法

概述

生成器的核心是 yield 关键字。yield 类似于 return,但它不会结束函数执行,而是暂停函数并将一个值返回给调用者。当生成器被请求下一个值时,函数从暂停处继续执行。本章将深入讲解 yieldyield from 的各种语法形式,以及 Generator 类的核心方法。

关键概念

yield 的本质是"暂停并产出"。每次 yield 都会将一个值发送给调用者,同时暂停生成器函数的执行。

基础概念

yield 关键字

yield 是生成器函数中使用的特殊关键字,它使函数成为一个生成器。包含 yield 的函数会返回一个 Generator 对象,而非直接执行函数体。

yield from 关键字

yield from(PHP 7.0+)用于将另一个生成器、数组或 Traversable 对象的产出值委托到当前生成器,简化了嵌套生成器的编写。

Generator 类的方法

Generator 对象提供了以下核心方法:current()key()next()rewind()valid()send()getReturn()throw()

语法与代码

基本 yield

php
<?php
declare(strict_types=1);

function simpleYield(): \Generator
{
    yield 'first';
    yield 'second';
    yield 'third';
}

$gen = simpleYield();
echo $gen->current() . "\n";  // first
$gen->next();
echo $gen->current() . "\n";  // second
$gen->next();
echo $gen->current() . "\n";  // third

yield 键值对

php
<?php
declare(strict_types=1);

function keyValueYield(): \Generator
{
    yield 'id' => 1;
    yield 'name' => 'Alice';
    yield 'role' => 'admin';
}

foreach (keyValueYield() as $key => $value) {
    echo "{$key}: {$value}\n";
}
// id: 1
// name: Alice
// role: admin

yield 表达式(接收外部值)

yield 不仅是一个语句,也是一个表达式,可以接收调用者通过 send() 传入的值。

php
<?php
declare(strict_types=1);

function accumulator(): \Generator
{
    $total = 0;
    while (true) {
        $value = yield $total; // 产出当前总和,接收新值
        if ($value === null) {
            break;
        }
        $total += $value;
    }
}

$gen = accumulator();
echo $gen->current() . "\n"; // 0(初始值)
echo $gen->send(10) . "\n";  // 10(0 + 10)
echo $gen->send(20) . "\n";  // 30(10 + 20)
echo $gen->send(5) . "\n";   // 35(30 + 5)
$gen->send(null);             // 终止生成器

send() 的双重作用

Generator::send($value) 做了两件事:将 $value 传给当前的 yield 表达式,然后推进生成器到下一个 yield 并返回其值。

yield from 委托生成器

php
<?php
declare(strict_types=1);

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

function outerGenerator(): \Generator
{
    yield 'start';
    yield from innerGenerator(); // 委托给内部生成器
    yield 'end';
}

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

Generator::current() 和 Generator::key()

php
<?php
declare(strict_types=1);

function indexedGenerator(): \Generator
{
    yield 10 => 'ten';
    yield 20 => 'twenty';
    yield 30 => 'thirty';
}

$gen = indexedGenerator();

while ($gen->valid()) {
    echo "key={$gen->key()}, value={$gen->current()}\n";
    $gen->next();
}
// key=10, value=ten
// key=20, value=twenty
// key=30, value=thirty

Generator::getReturn()

php
<?php
declare(strict_types=1);

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

$gen = generatorWithReturn();

foreach ($gen as $value) {
    echo "{$value}\n";
}
// 1
// 2

echo $gen->getReturn(); // done

Generator::throw()

php
<?php
declare(strict_types=1);

function resilientGenerator(): \Generator
{
    try {
        yield 'before error';
        yield 'this will not be reached'; // 不会到达
    } catch (\InvalidArgumentException $e) {
        yield "caught: {$e->getMessage()}";
    }
    yield 'after catch';
}

$gen = resilientGenerator();
echo $gen->current() . "\n";  // before error
echo $gen->throw(new \InvalidArgumentException('test error')) . "\n";
// caught: test error
echo $gen->current() . "\n";  // after catch

详细说明

yield 的表达式形式

yield 有以下几种语法形式:

形式说明示例
yield $value产出值,键为自动递增的整数yield 'hello'
yield $key => $value产出键值对yield 'name' => 'Alice'
$received = yield产出 null,接收 send 的值$input = yield
$received = yield $value产出值,同时接收 send 的值$input = yield $total

send() 的执行流程

$gen->send($value) 的执行步骤:
1. 将 $value 赋给当前 yield 表达式
2. 恢复生成器执行(从当前 yield 处继续)
3. 执行到下一个 yield 或 return
4. 返回下一个 yield 的值(或 return 值)

yield from 的值传递

yield from 不仅可以委托产出值,还可以将从外部 send() 的值传递给子生成器,并获取子生成器的 return 值。

php
<?php
declare(strict_types=1);

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

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

$gen = mainGenerator();
echo $gen->current() . "\n";   // start
echo $gen->next() . "\n";      // from sub
echo $gen->send('hello') . "\n"; // sub returned: processed: hello

Generator 的 rewind() 限制

Generator::rewind() 只能在生成器尚未开始时调用(状态为 Created)。一旦生成器开始执行(首次调用 current()rewind() 之后),再次调用 rewind() 会抛出异常。

php
<?php
declare(strict_types=1);

function testRewind(): \Generator
{
    yield 1;
    yield 2;
}

$gen = testRewind();
$gen->rewind(); // 正确:首次调用
echo $gen->current(); // 1

// $gen->rewind(); // 异常:Cannot rewind a generator that was already started

实战示例

实战:协程任务调度器

php
<?php
declare(strict_types=1);

class Task
{
    private int $taskId;
    private \Generator $coroutine;
    private mixed $sendValue = null;

    public function __construct(int $taskId, \Generator $coroutine)
    {
        $this->taskId = $taskId;
        $this->coroutine = $coroutine;
    }

    public function getTaskId(): int
    {
        return $this->taskId;
    }

    public function run(): mixed
    {
        if ($this->sendValue !== null) {
            $result = $this->coroutine->send($this->sendValue);
            $this->sendValue = null;
        } else {
            $result = $this->coroutine->current();
        }
        return $result;
    }

    public function isFinished(): bool
    {
        return !$this->coroutine->valid();
    }

    public function send(mixed $value): void
    {
        $this->sendValue = $value;
    }
}

class Scheduler
{
    /** @var \SplQueue<Task> */
    private \SplQueue $taskQueue;

    public function __construct()
    {
        $this->taskQueue = new \SplQueue();
    }

    public function newTask(\Generator $coroutine): int
    {
        $taskId = $this->taskQueue->count() + 1;
        $this->taskQueue->enqueue(new Task($taskId, $coroutine));
        return $taskId;
    }

    public function run(): void
    {
        while (!$this->taskQueue->isEmpty()) {
            $task = $this->taskQueue->dequeue();
            $task->run();

            if (!$task->isFinished()) {
                $this->taskQueue->enqueue($task);
            }
        }
    }
}

$scheduler = new Scheduler();
$scheduler->newTask((function (): \Generator {
    for ($i = 1; $i <= 3; $i++) {
        echo "Task A: step {$i}\n";
        yield;
    }
})());
$scheduler->newTask((function (): \Generator {
    for ($i = 1; $i <= 3; $i++) {
        echo "Task B: step {$i}\n";
        yield;
    }
})());

$scheduler->run();

实战:XML 流式解析器

php
<?php
declare(strict_types=1);

function xmlElementGenerator(string $filePath, string $elementName): \Generator
{
    $reader = new \XMLReader();
    if (!$reader->open($filePath)) {
        throw new \RuntimeException("无法打开 XML 文件: {$filePath}");
    }

    try {
        while ($reader->read()) {
            if ($reader->nodeType === \XMLReader::ELEMENT
                && $reader->name === $elementName
            ) {
                $element = simplexml_load_string($reader->readOuterXml());
                if ($element !== false) {
                    yield (array) $element;
                }
            }
        }
    } finally {
        $reader->close();
    }
}

// 逐条解析大型 XML 文件中的特定元素
foreach (xmlElementGenerator('data.xml', 'product') as $product) {
    echo "商品: {$product['name']}\n";
}

注意事项

yield 只能在函数中使用

yield 只能在函数(包括方法、匿名函数)中使用。不能在全局作用域或非函数上下文中使用 yield

自动启动机制

Generator::current() 在首次调用时会自动启动生成器(相当于隐式调用 rewind())。因此,你不需要显式调用 rewind() 来开始迭代。

send() 的首次调用

在生成器尚未开始时调用 send(),其行为等同于 rewind() + send()。传入的值会被忽略(因为还没有 yield 表达式来接收它)。

最佳实践

  1. 使用 yield from 组合生成器:将复杂逻辑拆分为多个小生成器,通过 yield from 组合。
  2. 在生成器中使用 try/finally:确保外部资源(文件句柄、数据库连接)在生成器中断时被释放。
  3. 为生成器添加返回类型:始终声明返回类型为 \Generator
  4. 合理使用 send():只在需要双向通信时使用 send(),否则简单的 next() 足够。
  5. 避免在 yield 之间有副作用:保持生成器函数的纯粹性,避免在 yield 之间修改外部状态。
php
<?php
declare(strict_types=1);

/**
 * @param iterable<int> $source
 * @return \Generator<int, int>
 */
function runningAverage(iterable $source): \Generator
{
    $sum = 0;
    $count = 0;

    foreach ($source as $index => $value) {
        $sum += $value;
        $count++;
        yield $index => $sum / $count;
    }
}

foreach (runningAverage([10, 20, 30, 40, 50]) as $i => $avg) {
    echo "第{$i}轮平均值: {$avg}\n";
}

参考链接