PHP 迭代器协议深度剖析:Iterator、Generator 与 yield from 的内核链路

在 PHP 的日常开发中,foreach 可能是最熟悉的语法结构之一。但当你写下 foreach ($collection as $item) 时,PHP 引擎内部究竟发生了什么?Iterator 接口、Generator 以及 yield from 之间又存在怎样的内核链路?本文将从 Zend 引擎的实现层面,逐层剖析 PHP 迭代器协议的完整运作机制。

一、迭代器协议的根基:Traversable 与 Iterator

PHP 中所有可被 foreach 遍历的对象,都必须直接或间接实现 Traversable 接口。但 Traversable 是一个不可直接实现的“标记接口”——它是引擎层面的契约,PHP 规定用户代码不能直接实现它,只能通过 IteratorIteratorAggregate 间接实现。

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),并按照以下顺序调用方法:

  1. rewind() —— 将内部指针复位到起始位置
  2. valid() —— 检查当前位置是否有效
  3. current() —— 获取当前值
  4. key() —— 获取当前键
  5. next() —— 将指针前移
  6. 重复 2–5,直到 valid() 返回 false

这一过程在 zend_vm_def.hZEND_FE_RESET_RZEND_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_RUNNINGZEND_GENERATOR_FINISHED 等)

foreach 调用 Generator::current() 时,引擎读取 generator->value;调用 next() 时,引擎通过 zend_generator_resume() 恢复执行栈帧,从上次 yield 的位置继续运行,直到遇到下一个 yield 或函数返回。

关键点在于:生成器的挂起不是线程或协程级别的切换,而是 Zend 虚拟机指令级别的栈帧保存yield 在编译后对应 ZEND_GENERATOR_CREATEZEND_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 时,引擎会:

  1. 将当前生成器(outer)的 execute_data 压入 generator->delegated 字段
  2. $inner 作为子生成器,建立父子关系
  3. 后续的 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 的内核链路

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏