生成器与 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 = 25Iterator 实现
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";
}
// 输出同上代码量对比
| 方面 | Generator | Iterator |
|---|---|---|
| 代码行数 | ~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";详细说明
功能对比表
| 功能 | Generator | Iterator | IteratorAggregate |
|---|---|---|---|
| 实现方式 | 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)。
最佳实践
- 默认选择 Generator:大多数遍历场景下,Generator 更简洁、更高效。
- 需要重置时选择 Iterator:如果需要在遍历过程中重置或多次遍历,使用 Iterator。
- 混合使用:Generator 可以作为 Iterator 的内部数据源,两者并不互斥。
- Generator 替代简单 Iterator:如果你只需要实现基本的遍历逻辑,用 Generator 替代 Iterator 可以大幅减少代码量。
- 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;
}
}