Traversable / Iterator / IteratorAggregate 接口
概述
PHP 提供了一套完整的迭代器接口体系,允许对象像数组一样在 foreach 循环中被遍历。这套体系由三个核心接口组成:
- Traversable — 标记接口,内部引擎用于检测对象是否可迭代
- Iterator — 需要实现 5 个方法的完整迭代器接口
- IteratorAggregate — 更简洁的替代方案,只需返回一个迭代器
Traversable 是最顶层接口,不能直接被用户类实现(只能由内部类实现)。用户类需要实现 Iterator 或 IteratorAggregate 来获得 foreach 遍历能力。
版本说明
迭代器接口从 PHP 5.0 起可用,是 SPL(Standard PHP Library)的核心组成部分。PHP 8.1+ 中行为无变化,但配合 Generator 和 Fiber 等新特性,迭代器体系的应用场景更加丰富。
基础概念
Traversable 接口
Traversable 是一个空的标记接口(marker interface),不包含任何方法。它的唯一作用是让 PHP 引擎识别一个对象可以在 foreach 中使用。
php
<?php
declare(strict_types=1);
// Traversable 接口定义(简化)
// interface Traversable {}
// 注意:不能直接 implements Traversable
// 只能实现 Iterator 或 IteratorAggregateforeach 与迭代器的关系
当 foreach 遍历一个对象时,PHP 引擎会检查该对象是否实现了 Traversable 接口(即实现了 Iterator 或 IteratorAggregate),然后按照迭代器协议逐个获取元素。
foreach ($obj as $key => $value) {
// 等价于:
// $obj->rewind();
// while ($obj->valid()) {
// $key = $obj->key();
// $value = $obj->current();
// $obj->next();
// }
}Iterator 接口的五个方法
Iterator 接口继承自 Traversable,定义了遍历对象所需的全部方法。
接口定义
php
<?php
declare(strict_types=1);
// Iterator 接口的完整定义
// interface Iterator extends Traversable
// {
// public function current(): mixed;
// public function key(): mixed;
// public function next(): void;
// public function rewind(): void;
// public function valid(): bool;
// }各方法说明
| 方法 | 返回类型 | 调用时机 | 说明 |
|---|---|---|---|
rewind() | void | foreach 开始时 | 将指针重置到第一个元素 |
valid() | bool | 每次迭代开始前 | 检查当前指针位置是否有效 |
current() | mixed | 每次迭代体中 | 返回当前指针位置的元素值 |
key() | mixed | 每次迭代体中 | 返回当前指针位置的键名 |
next() | void | 每次迭代结束后 | 将指针移动到下一个位置 |
执行顺序
php
<?php
declare(strict_types=1);
// foreach 执行顺序演示
// 1. rewind() — 重置指针
// 2. valid() — 检查是否有效(无效则结束)
// 3. current() — 获取当前值
// 4. key() — 获取当前键
// 5. 执行循环体
// 6. next() — 移动到下一个
// 7. 回到第 2 步(valid())IteratorAggregate 接口
IteratorAggregate 是一种更简单的迭代方式,只需实现一个 getIterator() 方法,返回一个 Traversable 对象即可。
php
<?php
declare(strict_types=1);
// IteratorAggregate 接口定义
// interface IteratorAggregate extends Traversable
// {
// public function getIterator(): Traversable;
// }选择建议
- 使用
Iterator当需要精细控制迭代行为时 - 使用
IteratorAggregate当内部已有可迭代数据结构时 - 在性能敏感场景中,
IteratorAggregate通常更高效
详细说明
Iterator 实现示例
php
<?php
declare(strict_types=1);
class Bookshelf implements Iterator
{
/** @var array<int, string> */
private array $books = [];
private int $position = 0;
public function add(string $title): void
{
$this->books[] = $title;
}
public function rewind(): void
{
$this->position = 0;
}
public function valid(): bool
{
return isset($this->books[$this->position]);
}
public function current(): string
{
return $this->books[$this->position];
}
public function key(): int
{
return $this->position;
}
public function next(): void
{
$this->position++;
}
}
$shelf = new Bookshelf();
$shelf->add('PHP 编程');
$shelf->add('设计模式');
$shelf->add('数据结构');
foreach ($shelf as $index => $title) {
echo "{$index}: {$title}\n";
// 0: PHP 编程
// 1: 设计模式
// 2: 数据结构
}IteratorAggregate 实现示例
php
<?php
declare(strict_types=1);
class BookCollection implements IteratorAggregate
{
/** @var array<string, string> */
private array $books = [];
public function add(string $isbn, string $title): void
{
$this->books[$isbn] = $title;
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->books);
}
}
$collection = new BookCollection();
$collection->add('978-1', 'PHP 高级编程');
$collection->add('978-2', '重构');
$collection->add('978-3', '代码整洁之道');
foreach ($collection as $isbn => $title) {
echo "{$isbn}: {$title}\n";
// 978-1: PHP 高级编程
// 978-2: 重构
// 978-3: 代码整洁之道
}使用 Generator 作为迭代器(PHP 5.5+)
php
<?php
declare(strict_types=1);
class FibonacciSequence implements IteratorAggregate
{
private int $limit;
public function __construct(int $limit)
{
$this->limit = $limit;
}
public function getIterator(): Traversable
{
$a = 0;
$b = 1;
for ($i = 0; $i < $this->limit; $i++) {
yield $i => $a;
[$a, $b] = [$b, $a + $b];
}
}
}
$sequence = new FibonacciSequence(10);
foreach ($sequence as $index => $number) {
echo "F({$index}) = {$number}\n";
// F(0) = 0, F(1) = 1, F(2) = 1, F(3) = 2, ...
}实战示例
文件逐行读取迭代器
php
<?php
declare(strict_types=1);
class LineReader implements Iterator
{
private string $filePath;
private ?resource $handle = null;
private int $lineNumber = 0;
private string $currentLine = '';
private bool $isValid = false;
public function __construct(string $filePath)
{
$this->filePath = $filePath;
}
public function rewind(): void
{
if ($this->handle !== null) {
fclose($this->handle);
}
$handle = fopen($this->filePath, 'r');
if ($handle === false) {
throw new RuntimeException("无法打开文件: {$this->filePath}");
}
$this->handle = $handle;
$this->lineNumber = 0;
$this->readNextLine();
}
private function readNextLine(): void
{
assert($this->handle !== null);
$line = fgets($this->handle);
if ($line === false) {
$this->isValid = false;
$this->currentLine = '';
} else {
$this->isValid = true;
$this->currentLine = rtrim($line, "\r\n");
}
}
public function valid(): bool
{
return $this->isValid;
}
public function current(): string
{
return $this->currentLine;
}
public function key(): int
{
return $this->lineNumber;
}
public function next(): void
{
$this->lineNumber++;
$this->readNextLine();
}
public function __destruct()
{
if ($this->handle !== null) {
fclose($this->handle);
}
}
}分页数据迭代器
php
<?php
declare(strict_types=1);
class PaginatedResultIterator implements Iterator
{
/** @var array<int, mixed> */
private array $currentPageItems = [];
private int $position = 0;
private int $currentPage = 1;
private bool $exhausted = false;
public function __construct(
private readonly int $pageSize = 20
) {}
public function rewind(): void
{
$this->position = 0;
$this->currentPage = 1;
$this->exhausted = false;
$this->currentPageItems = $this->fetchPage($this->currentPage);
}
public function valid(): bool
{
if ($this->exhausted) {
return false;
}
if ($this->position < count($this->currentPageItems)) {
return true;
}
// 当前页已遍历完,尝试加载下一页
$this->currentPage++;
$this->currentPageItems = $this->fetchPage($this->currentPage);
if (empty($this->currentPageItems)) {
$this->exhausted = true;
return false;
}
$this->position = 0;
return true;
}
public function current(): mixed
{
return $this->currentPageItems[$this->position];
}
public function key(): int
{
return ($this->currentPage - 1) * $this->pageSize + $this->position;
}
public function next(): void
{
$this->position++;
}
/**
* @return array<int, mixed>
*/
private function fetchPage(int $page): array
{
// 实际项目中这里调用数据库或 API
$offset = ($page - 1) * $this->pageSize;
// 模拟数据:前3页有数据
if ($page > 3) {
return [];
}
return array_fill(0, $this->pageSize, "item_{$offset}");
}
}过滤迭代器(组合 FilterIterator)
php
<?php
declare(strict_types=1);
class EvenNumberIterator extends FilterIterator
{
public function accept(): bool
{
$current = $this->getInnerIterator()->current();
return is_int($current) && $current % 2 === 0;
}
}
class NumberRange implements IteratorAggregate
{
public function __construct(
private readonly int $start,
private readonly int $end
) {}
public function getIterator(): Traversable
{
return new EvenNumberIterator(
new ArrayIterator(range($this->start, $this->end))
);
}
}
$range = new NumberRange(1, 20);
foreach ($range as $number) {
echo "{$number} "; // 2 4 6 8 10 12 14 16 18 20
}注意事项
rewind() 的调用时机
php
<?php
declare(strict_types=1);
class LoggingIterator implements Iterator
{
/** @var array<int, string> */
private array $items;
private int $pos = 0;
public function __construct(array $items)
{
$this->items = $items;
}
public function rewind(): void
{
echo "rewind() 被调用\n";
$this->pos = 0;
}
public function valid(): bool { return isset($this->items[$this->pos]); }
public function current(): string { return $this->items[$this->pos]; }
public function key(): int { return $this->pos; }
public function next(): void { $this->pos++; }
}
$iter = new LoggingIterator(['a', 'b', 'c']);
// 注意:foreach 每次都会调用 rewind()
foreach ($iter as $v) {
// rewind() 被调用(只调用一次)
}
foreach ($iter as $v) {
// rewind() 被调用(第二次 foreach 再次调用)
}
// 嵌套 foreach 中的陷阱
$iter = new LoggingIterator(['x', 'y', 'z']);
foreach ($iter as $outer) {
foreach ($iter as $inner) {
// rewind() 在内层 foreach 开始时再次被调用
// 这会导致外层循环被重置!
break; // 必须小心
}
break;
}嵌套迭代陷阱
同一迭代器对象在嵌套 foreach 中使用时,内层 foreach 会调用 rewind() 重置指针,导致外层循环行为异常。解决方案:克隆迭代器或使用 IteratorAggregate 返回新的迭代器实例。
与 ArrayIterator 的性能对比
性能参考
- 原生数组
foreach:最快 ArrayIterator(内部 C 实现):接近原生数组性能- 自定义
Iterator(PHP 用户态实现):最慢,约慢 2~3 倍 IteratorAggregate+ArrayIterator:性能介于两者之间
最佳实践
- 优先使用 IteratorAggregate:当内部数据是数组时,使用
IteratorAggregate返回ArrayIterator更简洁高效 - 避免在迭代过程中修改数据:在
next()/rewind()中不应修改被遍历的数据结构 - 考虑使用 Generator:PHP 5.5+ 的
yield语法是实现迭代器最简洁的方式 - 实现 Countable 接口:迭代器类同时实现
Countable接口,可支持count()函数 - 注意嵌套 foreach 陷阱:需要嵌套遍历时,考虑克隆迭代器或返回新实例
php
<?php
declare(strict_types=1);
// 最佳实践:同时实现 Iterator 和 Countable
class UserCollection implements Iterator, Countable
{
/** @var array<int, array{id: int, name: string}> */
private array $users = [];
private int $position = 0;
public function add(int $id, string $name): void
{
$this->users[] = ['id' => $id, 'name' => $name];
}
// Countable
public function count(): int
{
return count($this->users);
}
// Iterator
public function rewind(): void { $this->position = 0; }
public function valid(): bool { return isset($this->users[$this->position]); }
public function current(): array { return $this->users[$this->position]; }
public function key(): int { return $this->position; }
public function next(): void { $this->position++; }
}