在 PHP 的日常开发中,foreach 可能是最熟悉的语法结构之一。但当你写下 foreach ($collection as $item) 时,PHP 引擎内部究竟发生了什么?Iterator 接口、Generator 以及 yield from 之间又存在怎样的内核链路?本文将从 Zend 引擎的实现层面,逐层剖析 PHP 迭代器协议的完整运作机制。
一、迭代器协议的根基:Traversable 与 Iterator
PHP 中所有可被 foreach 遍历的对象,都必须直接或间接实现 Traversable 接口。但 Traversable 是一个不可直接实现的“标记接口”——它是引擎层面的契约,PHP 规定用户代码不能直接实现它,只能通过 Iterator 或 IteratorAggregate 间接实现。
interface Traversable {}
interface Iterator extends Traversable {
public function current(): mixed;
public function key(): mixed;
public function next(): void;
public function rewind(): void;
public function valid(): bool;
}
interface IteratorAggregate extends Traversable {
public function getIterator(): Traversable;
}
当 foreach 遇到一个实现了 Iterator 的对象时,Zend 引擎会将其识别为“对象迭代器”(ZEND_ITER_OBJECT),并按照以下顺序调用方法:
rewind()—— 将内部指针复位到起始位置valid()—— 检查当前位置是否有效current()—— 获取当前值key()—— 获取当前键next()—— 将指针前移- 重复 2–5,直到
valid()返回false
这一过程在 zend_vm_def.h 的 ZEND_FE_RESET_R 和 ZEND_FE_FETCH_R 指令中实现。引擎会为每个 foreach 维护一个迭代状态句柄,对象迭代器的状态直接存储在对象自身的 properties 哈希表中。
IteratorAggregate 则是一个语法糖:引擎在 FE_RESET 阶段调用 getIterator(),拿到真正的 Iterator 后再走上述流程。它适合将迭代逻辑委托给内部数组或其他集合。
二、Generator:迭代器协议的语法级捷径
手写 Iterator 需要维护五个方法的状态机,代码冗长且易错。PHP 5.5 引入的 Generator 从根本上改变了这一点——它让函数在保留执行上下文的同时“暂停”和“恢复”。
function gen(): Generator {
yield 1 => 'a';
yield 2 => 'b';
return 'done';
}
当一个函数体内出现 yield 时,Zend 编译器会将该函数标记为生成器,并在编译阶段生成一个 zend_generator 结构体。调用该函数不会立即执行函数体,而是返回一个 Generator 对象,该对象同时实现了 Iterator 接口——这意味着它天然就是 foreach 可遍历的。
内核状态机
zend_generator 的核心字段包括:
execute_data:保存暂停时的执行栈帧(EG(current_execute_data) 的快照)node:指向当前yield对应的zend_ast节点value/key:当前产出的值与键flags:记录生成器状态(ZEND_GENERATOR_CURRENTLY_RUNNING、ZEND_GENERATOR_FINISHED等)
当 foreach 调用 Generator::current() 时,引擎读取 generator->value;调用 next() 时,引擎通过 zend_generator_resume() 恢复执行栈帧,从上次 yield 的位置继续运行,直到遇到下一个 yield 或函数返回。
关键点在于:生成器的挂起不是线程或协程级别的切换,而是 Zend 虚拟机指令级别的栈帧保存。yield 在编译后对应 ZEND_GENERATOR_CREATE、ZEND_YIELD 等 opcode,恢复时通过 zend_generator_resume 重新进入执行循环。
三、yield from:委托迭代的内核链路
PHP 7.0 引入的 yield from 是生成器协议的进一步延伸。它允许一个生成器将迭代委托给另一个可遍历结构:
function inner(): Generator {
yield 1;
yield 2;
}
function outer(): Generator {
yield 0;
yield from inner();
yield 3;
}
yield from 的内核实现远比表面复杂。它并非简单地展开为循环,而是在 zend_generator 中引入了委托链(delegation chain)。
委托链的结构
当执行到 yield from $inner 时,引擎会:
- 将当前生成器(
outer)的execute_data压入generator->delegated字段 - 将
$inner作为子生成器,建立父子关系 - 后续的
current()、key()、next()调用会穿透到子生成器
在 zend_generator.c 中,zend_generator_get_current() 函数负责沿着委托链向下查找当前活跃的生成器。当子生成器迭代完毕后,引擎通过 zend_generator_ensure_initialized() 和 zend_generator_resume() 恢复父生成器的执行。
返回值传递
yield from 还有一个容易被忽略的特性:子生成器的 return 值会成为 yield from 表达式的值。
function inner(): Generator {
yield 1;
return 'inner_result';
}
function outer(): Generator {
$result = yield from inner();
echo $result; // inner_result
yield 2;
}
在内核层面,子生成器返回时,其 return 值被写入 generator->retval,父生成器恢复执行后通过 ZEND_GENERATOR_RETURN opcode 读取该值并压入栈顶,作为 yield from 表达式的结果。
四、性能对比与选型建议
从内核实现可以推导出三者的性能特征:
| 机制 | 状态存储 | 方法调用开销 | 适用场景 |
|---|---|---|---|
| 手写 Iterator | 对象属性 | 5 次方法调用/轮 | 复杂状态机、需复用 |
| Generator | 栈帧快照 | 引擎内联 | 惰性求值、流式处理 |
| yield from | 委托链 | 穿透查找 | 生成器组合、递归遍历 |
生成器的优势在于零方法调用开销——foreach 遍历生成器时,引擎直接读取 zend_generator 的内部字段,而非通过 zend_std_call_user_function 走完整的用户函数调用路径。这也是为什么在处理大文件读取、数据库游标、无限序列时,生成器比手写 Iterator 快 2–3 倍的原因。
而 yield from 的委托链虽然引入了查找开销,但它避免了手动循环展开的代码冗余,在递归目录遍历、树形结构扁平化等场景下,可读性收益远大于性能损耗。
五、总结
PHP 迭代器协议的内核链路可以概括为:
Traversable(引擎契约)→ Iterator(方法协议)→ Generator(语法级实现)→ yield from(委托组合)
理解这条链路的意义在于:当你写出 foreach 时,你不仅是在使用一个语法糖,更是在与 Zend 虚拟机的迭代状态机对话。生成器不是魔法,它只是把栈帧保存与恢复的复杂性封装在了 yield 关键字背后。而 yield from 则是在此基础上,用委托链构建了一层可组合的迭代抽象。
掌握这些内核细节,你就能在性能敏感的场景下做出更精准的技术选型,也能在调试生成器行为时,从“黑盒猜测”转向“白盒推理”。
未经允许不得转载:任鹏个人博客 » PHP 迭代器协议深度剖析:Iterator、Generator 与 yield from 的内核链路

