Skip to content

Traversable / Iterator / IteratorAggregate 接口

概述

PHP 提供了一套完整的迭代器接口体系,允许对象像数组一样在 foreach 循环中被遍历。这套体系由三个核心接口组成:

  • Traversable — 标记接口,内部引擎用于检测对象是否可迭代
  • Iterator — 需要实现 5 个方法的完整迭代器接口
  • IteratorAggregate — 更简洁的替代方案,只需返回一个迭代器

Traversable 是最顶层接口,不能直接被用户类实现(只能由内部类实现)。用户类需要实现 IteratorIteratorAggregate 来获得 foreach 遍历能力。

版本说明

迭代器接口从 PHP 5.0 起可用,是 SPL(Standard PHP Library)的核心组成部分。PHP 8.1+ 中行为无变化,但配合 GeneratorFiber 等新特性,迭代器体系的应用场景更加丰富。

基础概念

Traversable 接口

Traversable 是一个空的标记接口(marker interface),不包含任何方法。它的唯一作用是让 PHP 引擎识别一个对象可以在 foreach 中使用。

php
<?php

declare(strict_types=1);

// Traversable 接口定义(简化)
// interface Traversable {}

// 注意:不能直接 implements Traversable
// 只能实现 Iterator 或 IteratorAggregate

foreach 与迭代器的关系

foreach 遍历一个对象时,PHP 引擎会检查该对象是否实现了 Traversable 接口(即实现了 IteratorIteratorAggregate),然后按照迭代器协议逐个获取元素。

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()voidforeach 开始时将指针重置到第一个元素
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:性能介于两者之间

最佳实践

  1. 优先使用 IteratorAggregate:当内部数据是数组时,使用 IteratorAggregate 返回 ArrayIterator 更简洁高效
  2. 避免在迭代过程中修改数据:在 next()/rewind() 中不应修改被遍历的数据结构
  3. 考虑使用 Generator:PHP 5.5+ 的 yield 语法是实现迭代器最简洁的方式
  4. 实现 Countable 接口:迭代器类同时实现 Countable 接口,可支持 count() 函数
  5. 注意嵌套 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++; }
}

参考链接