Skip to content

生成器总览

概述

生成器(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
<?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
<?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
<?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
<?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 类实现了 IteratorTraversable 接口,但它与普通的迭代器有本质区别:

特性Generator普通 Iterator
内存使用极低(惰性求值)通常需要存储所有数据
状态管理自动(PHP 内部维护)手动实现
代码量极少(yield 即可)较多(实现5个方法)
前进方向只能向前可以实现 rewind
实现复杂度简单复杂

Generator 的内部状态

生成器有以下几种状态:

  1. Created:生成器已创建但尚未启动
  2. Running:生成器正在执行(当前在生成器函数内部)
  3. Suspended:生成器在 yield 处暂停,等待恢复
  4. Finished:生成器函数执行完毕或 return

生成器的执行流程

调用生成器函数 → 返回 Generator 对象(状态:Created)
foreach 调用 current() → 启动生成器(状态:Running)
                          遇到 yield → 暂停(状态:Suspended),返回 yield 值
foreach 调用 next() → 恢复执行(状态:Running)
                          遇到 yield → 暂停(状态:Suspended)
                          遇到 return → 结束(状态:Finished)

实战示例

实战:无限序列

php
<?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
<?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
<?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
<?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
<?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
<?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); // 输出: 生成器清理完成

最佳实践

  1. 处理大数据集时优先使用生成器:对于大型数据集(文件、数据库结果、API 响应),生成器可以大幅降低内存使用。
  2. 使用 yield from 委托子任务:将复杂生成器拆分为多个小生成器,通过 yield from 组合。
  3. 注意生成器的单次迭代限制:如果需要多次访问数据,考虑使用 iterator_to_array() 或重新创建生成器。
  4. 生成器中使用 try/finally:确保资源在生成器中断时被正确释放。
  5. 为生成器函数添加类型提示:返回类型声明为 \Generator 或更具体的类型。
  6. 避免在生成器中使用全局状态:生成器应该是无副作用的,避免依赖和修改全局变量。
php
<?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);
    }
}

参考链接