生成器总览
概述
生成器(Generator)是 PHP 5.5 引入的一种强大的语言特性,允许你在函数中使用 yield 关键字来生成一系列值,而无需在内存中构建整个数组。生成器实现了惰性求值(Lazy Evaluation),只有当需要下一个值时才会计算并返回,这使得处理大数据集、无限序列和流式数据时具有极高的内存效率。
PHP 版本要求
生成器自 PHP 5.5 引入。PHP 7.0 增加了 Generator::getReturn(),PHP 7.1 增加了 yield from 委托生成器。本文基于 PHP 8.1+ 编写。
基础概念
什么是生成器
生成器是一种特殊的函数,它不像普通函数那样返回一个值然后结束,而是可以"暂停"执行,每次产出一个值(yield),并在下次调用时从暂停处继续执行。
Generator 类
每个生成器函数返回一个 Generator 对象,该对象实现了 Iterator 接口,因此可以像迭代器一样在 foreach 中使用。
惰性求值
惰性求值(Lazy Evaluation)是指值只在被需要时才计算。生成器正是这种模式的实现——它不会预先计算所有值,而是按需生成。
内存效率
由于生成器不需要一次性将所有数据加载到内存中,它在处理大数据集时的内存占用非常低。例如,遍历一个 10GB 的日志文件,使用生成器只需要很少的内存。
语法与代码
基本生成器
<?php
declare(strict_types=1);
function rangeGenerator(int $start, int $end): \Generator
{
for ($i = $start; $i <= $end; $i++) {
yield $i;
}
}
// 使用 foreach 遍历生成器
foreach (rangeGenerator(1, 5) as $number) {
echo "{$number} ";
}
// 输出: 1 2 3 4 5生成器与数组的内存对比
<?php
declare(strict_types=1);
// 普通函数:返回完整数组(全部加载到内存)
function rangeArray(int $start, int $end): array
{
$result = [];
for ($i = $start; $i <= $end; $i++) {
$result[] = $i;
}
return $result;
}
// 生成器函数:每次只产出一个值
function rangeGen(int $start, int $end): \Generator
{
for ($i = $start; $i <= $end; $i++) {
yield $i;
}
}
// 内存差异:rangeArray(1, 1000000) 占用约 32MB
// rangeGen(1, 1000000) 几乎不占用内存内存对比
rangeArray(1, 1000000)会在内存中创建一个包含 100 万个整数的数组,占用约 32-48MB。rangeGen(1, 1000000)每次只保留一个整数的内存,内存开销几乎为零。
生成器处理大文件
<?php
declare(strict_types=1);
function readLargeFile(string $path): \Generator
{
$handle = fopen($path, 'r');
if ($handle === false) {
throw new \RuntimeException("无法打开文件: {$path}");
}
while (!feof($handle)) {
yield fgets($handle);
}
fclose($handle);
}
// 逐行读取大文件,内存占用极低
foreach (readLargeFile('/var/log/access.log') as $line) {
if (str_contains($line, 'ERROR')) {
echo trim($line) . "\n";
}
}带键值的生成器
<?php
declare(strict_types=1);
function fruitGenerator(): \Generator
{
$fruits = [
'apple' => 'red',
'banana' => 'yellow',
'cherry' => 'red',
'grape' => 'purple',
];
foreach ($fruits as $name => $color) {
yield $name => $color;
}
}
foreach (fruitGenerator() as $name => $color) {
echo "{$name} is {$color}\n";
}
// apple is red
// banana is yellow
// cherry is red
// grape is purple详细说明
Generator 对象的特性
Generator 类实现了 Iterator 和 Traversable 接口,但它与普通的迭代器有本质区别:
| 特性 | Generator | 普通 Iterator |
|---|---|---|
| 内存使用 | 极低(惰性求值) | 通常需要存储所有数据 |
| 状态管理 | 自动(PHP 内部维护) | 手动实现 |
| 代码量 | 极少(yield 即可) | 较多(实现5个方法) |
| 前进方向 | 只能向前 | 可以实现 rewind |
| 实现复杂度 | 简单 | 复杂 |
Generator 的内部状态
生成器有以下几种状态:
- Created:生成器已创建但尚未启动
- Running:生成器正在执行(当前在生成器函数内部)
- Suspended:生成器在 yield 处暂停,等待恢复
- Finished:生成器函数执行完毕或 return
生成器的执行流程
调用生成器函数 → 返回 Generator 对象(状态:Created)
foreach 调用 current() → 启动生成器(状态:Running)
遇到 yield → 暂停(状态:Suspended),返回 yield 值
foreach 调用 next() → 恢复执行(状态:Running)
遇到 yield → 暂停(状态:Suspended)
遇到 return → 结束(状态:Finished)实战示例
实战:无限序列
<?php
declare(strict_types=1);
function fibonacciSequence(): \Generator
{
$a = 0;
$b = 1;
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
// 取前 10 个斐波那契数
$count = 0;
foreach (fibonacciSequence() as $number) {
echo "{$number} ";
if (++$count >= 10) {
break; // 无限生成器必须手动终止
}
}
// 输出: 0 1 1 2 3 5 8 13 21 34实战:CSV 解析器
<?php
declare(strict_types=1);
function parseCsvGenerator(string $filePath, string $delimiter = ','): \Generator
{
$handle = fopen($filePath, 'r');
if ($handle === false) {
throw new \RuntimeException("无法打开文件: {$filePath}");
}
$headers = fgetcsv($handle, 1000, $delimiter);
if ($headers === false) {
fclose($handle);
return;
}
while (($row = fgetcsv($handle, 1000, $delimiter)) !== false) {
if (count($row) === count($headers)) {
yield array_combine($headers, $row);
}
}
fclose($handle);
}
// 使用生成器逐行解析大型 CSV 文件
foreach (parseCsvGenerator('users.csv') as $user) {
echo "{$user['name']} ({$user['email']})\n";
}实战:分页数据源
<?php
declare(strict_types=1);
function paginatedGenerator(callable $fetchPage, int $pageSize = 100): \Generator
{
$page = 1;
while (true) {
$items = $fetchPage($page, $pageSize);
if (empty($items)) {
break;
}
foreach ($items as $item) {
yield $item;
}
$page++;
}
}
// 模拟 API 分页请求
$allItems = paginatedGenerator(
function (int $page, int $size): array {
// 模拟从 API 获取分页数据
$total = 253;
$start = ($page - 1) * $size;
$items = [];
for ($i = $start; $i < min($start + $size, $total); $i++) {
$items[] = ['id' => $i, 'value' => "item_{$i}"];
}
return $items;
},
50
);
foreach ($allItems as $item) {
// 处理每一项,无需关心分页逻辑
echo "ID: {$item['id']}\n";
}实战:管道模式(Pipeline)
<?php
declare(strict_types=1);
function pipeline(\Generator $source, callable ...$stages): \Generator
{
$current = $source;
foreach ($stages as $stage) {
$current = $stage($current);
}
yield from $current;
}
function mapGenerator(\Generator $input, callable $mapper): \Generator
{
foreach ($input as $item) {
yield $mapper($item);
}
}
function filterGenerator(\Generator $input, callable $filter): \Generator
{
foreach ($input as $item) {
if ($filter($item)) {
yield $item;
}
}
}
// 组合管道
$source = (function (): \Generator {
yield 1; yield 2; yield 3; yield 4; yield 5;
yield 6; yield 7; yield 8; yield 9; yield 10;
})();
$result = pipeline(
$source,
fn(\Generator $g) => filterGenerator($g, fn(int $v): bool => $v % 2 === 0),
fn(\Generator $g) => mapGenerator($g, fn(int $v): int => $v * $v),
);
foreach ($result as $value) {
echo "{$value} "; // 4 16 36 64 100
}注意事项
生成器只能迭代一次
一次性迭代
生成器一旦被迭代完毕,就不能再次迭代。如果需要多次迭代,要么重新创建生成器,要么使用 iterator_to_array() 将结果保存到数组中。
<?php
declare(strict_types=1);
function simpleGen(): \Generator
{
yield 1;
yield 2;
yield 3;
}
$gen = simpleGen();
foreach ($gen as $v) {
echo $v . " ";
}
// 输出: 1 2 3
// 再次遍历——没有输出(生成器已耗尽)
foreach ($gen as $v) {
echo $v . " ";
}
// 无输出生成器不能使用 return 返回数组
生成器中的 return 语句用于指定最终返回值(可通过 getReturn() 获取),而非直接返回数组。要"返回"多个值,应使用多次 yield。
生成器中的异常处理
<?php
declare(strict_types=1);
function safeGenerator(): \Generator
{
try {
for ($i = 0; $i < 5; $i++) {
yield $i;
}
} finally {
// 无论生成器如何结束(包括迭代中断),finally 都会执行
echo "生成器清理完成\n";
}
}
$gen = safeGenerator();
echo $gen->current(); // 0
unset($gen); // 输出: 生成器清理完成最佳实践
- 处理大数据集时优先使用生成器:对于大型数据集(文件、数据库结果、API 响应),生成器可以大幅降低内存使用。
- 使用
yield from委托子任务:将复杂生成器拆分为多个小生成器,通过yield from组合。 - 注意生成器的单次迭代限制:如果需要多次访问数据,考虑使用
iterator_to_array()或重新创建生成器。 - 生成器中使用 try/finally:确保资源在生成器中断时被正确释放。
- 为生成器函数添加类型提示:返回类型声明为
\Generator或更具体的类型。 - 避免在生成器中使用全局状态:生成器应该是无副作用的,避免依赖和修改全局变量。
<?php
declare(strict_types=1);
/**
* @return \Generator<int, string>
*/
function lineGenerator(string $filePath): \Generator
{
$handle = fopen($filePath, 'r');
if ($handle === false) {
return;
}
try {
while (($line = fgets($handle)) !== false) {
yield $line;
}
} finally {
fclose($handle);
}
}