Skip to content

生成器与 Iterator 比较

概述

生成器(Generator)和迭代器(Iterator)都是 PHP 中处理数据序列的机制,但它们的设计理念和适用场景有显著差异。生成器通过 yield 关键字以极少的代码实现惰性求值,而迭代器通过实现 Iterator 接口提供更精细的控制。本章将对比两者的实现方式、使用场景和优劣。

选择建议

大多数情况下,生成器是更好的选择。只有在你需要复杂的遍历逻辑(如双向遍历、缓存、可重置)时,才考虑使用迭代器。

基础概念

Generator 的特点

  • 使用 yield 关键字自动实现
  • 只能向前遍历
  • 不能重置(rewind 在首次后无效)
  • 内存效率极高
  • 代码量极少

Iterator 的特点

  • 需要实现 5 个方法
  • 支持完整的迭代器控制
  • 可以支持 rewind、双向遍历
  • 可以被多次遍历(如果实现了正确的 rewind)
  • 实现复杂度较高

语法与代码

生成器实现

php
<?php
declare(strict_types=1);

function rangeGenerator(int $start, int $end): \Generator
{
    for ($i = $start; $i <= $end; $i++) {
        yield $i => $i * $i;
    }
}

// 使用:极简
foreach (rangeGenerator(1, 5) as $num => $square) {
    echo "{$num}^2 = {$square}\n";
}
// 1^2 = 1
// 2^2 = 4
// 3^2 = 9
// 4^2 = 16
// 5^2 = 25

Iterator 实现

php
<?php
declare(strict_types=1);

class RangeIterator implements \Iterator
{
    private int $start;
    private int $end;
    private int $current;

    public function __construct(int $start, int $end)
    {
        $this->start = $start;
        $this->end = $end;
        $this->current = $start;
    }

    public function current(): int
    {
        return $this->current * $this->current;
    }

    public function key(): int
    {
        return $this->current;
    }

    public function next(): void
    {
        $this->current++;
    }

    public function rewind(): void
    {
        $this->current = $this->start;
    }

    public function valid(): bool
    {
        return $this->current <= $this->end;
    }
}

// 使用:需要创建对象
$iterator = new RangeIterator(1, 5);
foreach ($iterator as $num => $square) {
    echo "{$num}^2 = {$square}\n";
}
// 输出同上

代码量对比

方面GeneratorIterator
代码行数~5 行~30+ 行
状态管理自动手动
实现难度中-高
可读性

多次遍历对比

php
<?php
declare(strict_types=1);

// Iterator 可以多次遍历
$iterator = new RangeIterator(1, 3);
echo "第一次遍历: ";
foreach ($iterator as $v) { echo $v . " "; }
echo "\n";

$iterator->rewind(); // 重置
echo "第二次遍历: ";
foreach ($iterator as $v) { echo $v . " "; }
echo "\n";

// Generator 不能重置,需要重新创建
function simpleGen(): \Generator {
    yield 1; yield 2; yield 3;
}

echo "第一次遍历: ";
foreach (simpleGen() as $v) { echo $v . " "; }
echo "\n";

echo "第二次遍历: ";
foreach (simpleGen() as $v) { echo $v . " "; } // 重新创建
echo "\n";

详细说明

功能对比表

功能GeneratorIteratorIteratorAggregate
实现方式yield 关键字5 个方法getIterator()
惰性求值是(如果实现正确)
可重置取决于内部迭代器
双向遍历可以实现可以实现
缓存支持可以实现可以实现
send() 支持
代码量极少较多中等
多次遍历需重建支持取决于实现
内部状态PHP 管理开发者管理委托管理

性能对比

php
<?php
declare(strict_types=1);

// 性能测试框架
function benchmark(string $label, callable $fn): float
{
    $start = hrtime(true);
    for ($i = 0; $i < 10000; $i++) {
        $fn();
    }
    return (hrtime(true) - $start) / 1e6; // 毫秒
}

// Generator 性能
function genRange(int $n): \Generator
{
    for ($i = 0; $i < $n; $i++) {
        yield $i;
    }
}

// Iterator 性能
class IntIterator implements \Iterator
{
    private int $max;
    private int $i = 0;

    public function __construct(int $max)
    {
        $this->max = $max;
    }

    public function current(): int { return $this->i; }
    public function key(): int { return $this->i; }
    public function next(): void { $this->i++; }
    public function rewind(): void { $this->i = 0; }
    public function valid(): bool { return $this->i < $this->max; }
}

$n = 1000;
$genTime = benchmark('Generator', fn() => iterator_to_array(genRange($n)));
$iterTime = benchmark('Iterator', fn() => iterator_to_array(new IntIterator($n)));

echo "Generator: {$genTime}ms\n";
echo "Iterator:  {$iterTime}ms\n";

性能结论

在大多数场景下,Generator 的性能与 Iterator 相当或略优。Generator 的主要优势在于代码简洁和内存效率,而非执行速度。

使用场景对比

适合使用 Generator 的场景

  • 读取大文件或流数据
  • 生成无限序列
  • 处理数据库结果集(逐行处理)
  • 管道/过滤/映射操作
  • 协程和任务调度
  • 一次性遍历的场景

适合使用 Iterator 的场景

  • 需要多次遍历同一数据集
  • 需要双向遍历(向前和向后)
  • 需要在遍历中缓存元素
  • 需要实现复杂的遍历逻辑
  • 需要可重置的遍历

实战示例

实战:Generator 实现日志解析器

php
<?php
declare(strict_types=1);

function parseLogs(string $path): \Generator
{
    $handle = fopen($path, 'r');
    if ($handle === false) {
        return;
    }

    try {
        while (($line = fgets($handle)) !== false) {
            $line = trim($line);
            if ($line === '' || str_starts_with($line, '#')) {
                continue;
            }

            $parts = explode('|', $line);
            if (count($parts) >= 3) {
                yield [
                    'timestamp' => $parts[0],
                    'level' => $parts[1],
                    'message' => $parts[2],
                ];
            }
        }
    } finally {
        fclose($handle);
    }
}

// 使用 Generator 的简洁语法
foreach (parseLogs('app.log') as $entry) {
    if ($entry['level'] === 'ERROR') {
        echo "[{$entry['timestamp']}] {$entry['message']}\n";
    }
}

实战:Iterator 实现可重置的集合

php
<?php
declare(strict_types=1);

class FilterableCollection implements \Iterator
{
    /** @var array<int, mixed> */
    private array $items;
    private int $position = 0;
    private ?callable $filter;

    /** @var array<int, mixed> */
    private array $filtered = [];

    public function __construct(array $items, ?callable $filter = null)
    {
        $this->items = $items;
        $this->filter = $filter;
        $this->buildFiltered();
    }

    private function buildFiltered(): void
    {
        $this->filtered = $this->filter !== null
            ? array_filter($this->items, $this->filter)
            : $this->items;
        $this->filtered = array_values($this->filtered);
    }

    public function setFilter(callable $filter): void
    {
        $this->filter = $filter;
        $this->buildFiltered();
        $this->rewind();
    }

    public function current(): mixed { return $this->filtered[$this->position]; }
    public function key(): int { return $this->position; }
    public function next(): void { $this->position++; }
    public function rewind(): void { $this->position = 0; }
    public function valid(): bool { return isset($this->filtered[$this->position]); }
}

$collection = new FilterableCollection(
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    fn(int $v): bool => $v % 2 === 0
);

// 可以多次遍历
echo "第一次: ";
foreach ($collection as $v) { echo $v . " "; }
// 第一次: 2 4 6 8 10

$collection->rewind();
echo "\n第二次: ";
foreach ($collection as $v) { echo $v . " "; }
// 第二次: 2 4 6 8 10

// 动态修改过滤条件
$collection->setFilter(fn(int $v): bool => $v > 5);
echo "\n过滤后: ";
foreach ($collection as $v) { echo $v . " "; }
// 过滤后: 6 7 8 9 10

实战:两者混合使用

php
<?php
declare(strict_types=1);

// 生成器作为 Iterator 的数据源
class PaginatedResult implements \Iterator
{
    private int $currentPage = 1;
    private int $pageSize;
    private ?\Generator $currentPageData = null;
    private int $position = 0;

    public function __construct(
        private readonly callable $fetchPage,
        int $pageSize = 100
    ) {
        $this->pageSize = $pageSize;
        $this->loadPage();
    }

    private function loadPage(): void
    {
        $data = ($this->fetchPage)($this->currentPage, $this->pageSize);
        $this->currentPageData = (function () use ($data): \Generator {
            foreach ($data as $item) {
                yield $item;
            }
        })();
    }

    public function current(): mixed
    {
        return $this->currentPageData->current();
    }

    public function key(): int
    {
        return ($this->currentPage - 1) * $this->pageSize + $this->position;
    }

    public function next(): void
    {
        $this->position++;
        $this->currentPageData->next();

        if (!$this->currentPageData->valid()) {
            $this->currentPage++;
            $this->loadPage();
        }
    }

    public function rewind(): void
    {
        $this->currentPage = 1;
        $this->position = 0;
        $this->loadPage();
    }

    public function valid(): bool
    {
        return $this->currentPageData !== null && $this->currentPageData->valid();
    }
}

注意事项

Generator 的 send() 优势

Generator 支持 send() 方法实现双向通信,这是标准 Iterator 不具备的。如果需要调用者与数据源之间双向通信,必须使用 Generator。

Iterator 的 rewind() 优势

Iterator 的 rewind() 方法允许重新遍历同一数据集,Generator 则不能。如果需要多次遍历,Iterator 是更好的选择。

接口兼容性

两者都实现了 Traversable 接口,因此都可以在 foreach 中使用。Generator 还额外实现了 Generator 接口(扩展自 Iterator)。

最佳实践

  1. 默认选择 Generator:大多数遍历场景下,Generator 更简洁、更高效。
  2. 需要重置时选择 Iterator:如果需要在遍历过程中重置或多次遍历,使用 Iterator。
  3. 混合使用:Generator 可以作为 Iterator 的内部数据源,两者并不互斥。
  4. Generator 替代简单 Iterator:如果你只需要实现基本的遍历逻辑,用 Generator 替代 Iterator 可以大幅减少代码量。
  5. Iterator 用于复杂状态管理:当遍历逻辑涉及复杂的状态管理(如分页、缓存、跳转)时,Iterator 更合适。
php
<?php
declare(strict_types=1);

// 推荐模式:将 Generator 用于一次性数据处理
function transformData(iterable $source): \Generator
{
    foreach ($source as $item) {
        if ($item !== null) {
            yield transform($item);
        }
    }
}

// 推荐模式:将 Iterator 用于可重用的集合
class SearchableCollection implements \IteratorAggregate
{
    public function __construct(private readonly array $items) {}

    public function getIterator(): \Traversable
    {
        return new \ArrayIterator($this->items);
    }

    public function filter(callable $predicate): self
    {
        return new self(array_filter($this->items, $predicate));
    }

    public function first(): mixed
    {
        return $this->items[array_key_first($this->items)] ?? null;
    }
}

参考链接