应让类实现 IteratorAggregate 或 Iterator 接口;前者通过 getIterator() 返回内置迭代器(如 ArrayIterator)实现轻量遍历,后者需完整实现 5 个方法以支持动态结构与状态控制。

为什么 foreach 会报 Fatal error: Cannot use object of type Xxx as array
当你把一个自定义类实例直接丢进 foreach,PHP 默认不认它是个“可遍历的东西”。它既不是数组,也没实现 Iterator 或 Traversable 接口,PHP 就直接抛错。这不是语法写错了,是类型契约没兑现——foreach 要求对象必须声明自己支持迭代。
常见诱因包括:封装了 array 属性但没暴露访问方式;数据结构嵌套了多层(比如树、图、带元信息的列表);需要延迟加载或过滤逻辑不能在构造时完成。
- 别试图用
(array)$obj强转——会丢失方法、私有属性、引用关系,且对嵌套对象无效 - 别在类里加
__get()模拟数组下标——foreach根本不调用它 - 真正解法是让类实现
Iterator接口,或更轻量的IteratorAggregate
用 IteratorAggregate 实现最小可行遍历
比起直接实现全部 5 个 Iterator 方法(current、key、next、rewind、valid),IteratorAggregate 只需一个 getIterator() 方法返回已有迭代器,干净又安全。
示例场景:一个管理用户集合的类,内部用 array 存储,但希望遍历时自动跳过被软删除的用户:
立即学习“PHP免费学习笔记(深入)”;
<?php
class UserCollection implements IteratorAggregate
{
private array $users = [];
public function add(User $user): void
{
$this->users[] = $user;
}
public function getIterator(): Traversable
{
// 过滤逻辑放在这里,而非在 foreach 循环体里
return new ArrayIterator(array_filter($this->users, fn($u) => !$u->isDeleted()));
}
}
-
ArrayIterator是 PHP 内置的可靠实现,不用自己手写边界检查 - 过滤发生在
getIterator(),意味着每次foreach都拿到干净数据,调用方无感知 - 如果内部数据源是数据库游标或生成器,这里也可以返回
CallbackFilterIterator或Generator
当需要控制遍历状态时,必须实现完整 Iterator
如果你的集合不是静态数组,而是动态结构(如链表、二叉树、分页结果集),或者要支持 reset()、key() 返回非数字索引、或遍历时修改内部状态(如读取文件指针),就得老老实实实现全部 5 个方法。
关键陷阱:rewind() 不只是重置计数器——它必须确保后续第一次 valid() 返回 true,否则 foreach 直接退出,连第一项都不执行。
-
current()和key()在valid() === false时行为未定义,务必保证只在有效状态下调用 - 不要在
next()里做耗时操作(如查库),foreach每次循环都会调它一次 - 若内部用
Generator,优先用yield+IteratorAggregate组合,比手写Iterator更少出错
Iterator 与 Generator 的性能和语义差异
两者都能让对象被 foreach 消费,但底层机制完全不同:Iterator 是状态机对象,Generator 是协程式函数。选错会导致内存爆炸或逻辑错乱。
典型误用:用 Generator 包裹大数组并 yield 全部元素——这反而比 ArrayIterator 多一层函数调用开销,且无法 rewind(Generator 只能单向消费)。
- 适合
Generator的场景:流式处理(逐行读文件)、API 分页拉取、递归树遍历(避免栈溢出) - 适合
Iterator的场景:需要多次遍历、支持count()、要兼容iterator_to_array()等函数 -
Generator无法被count()直接计算长度,除非你额外实现Countable接口
复杂集合的遍历从来不是“能不能跑起来”的问题,而是“谁负责维护状态”“何时触发副作用”“是否允许多次消费”这些隐含契约是否被显式表达清楚。漏掉任意一环,后期 debug 就得靠 var_dump 猜状态。



















