生成器语法
概述
生成器的核心是 yield 关键字。yield 类似于 return,但它不会结束函数执行,而是暂停函数并将一个值返回给调用者。当生成器被请求下一个值时,函数从暂停处继续执行。本章将深入讲解 yield、yield 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
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"; // thirdyield 键值对
<?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: adminyield 表达式(接收外部值)
yield 不仅是一个语句,也是一个表达式,可以接收调用者通过 send() 传入的值。
<?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
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 endGenerator::current() 和 Generator::key()
<?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=thirtyGenerator::getReturn()
<?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(); // doneGenerator::throw()
<?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
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: helloGenerator 的 rewind() 限制
Generator::rewind() 只能在生成器尚未开始时调用(状态为 Created)。一旦生成器开始执行(首次调用 current() 或 rewind() 之后),再次调用 rewind() 会抛出异常。
<?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
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
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 表达式来接收它)。
最佳实践
- 使用 yield from 组合生成器:将复杂逻辑拆分为多个小生成器,通过
yield from组合。 - 在生成器中使用 try/finally:确保外部资源(文件句柄、数据库连接)在生成器中断时被释放。
- 为生成器添加返回类型:始终声明返回类型为
\Generator。 - 合理使用 send():只在需要双向通信时使用
send(),否则简单的next()足够。 - 避免在 yield 之间有副作用:保持生成器函数的纯粹性,避免在 yield 之间修改外部状态。
<?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";
}