生成器返回键值对时如何同时迭代 key 与 value?
解读
国内一线/二线公司面试中,这道题常被放在“语言特性”环节,用来快速判断候选人是否真正用过生成器、是否理解 yield 的底层语义。
很多候选人只写过 yield $value,看到 yield $key => $value 会愣住;或者知道语法却说不清迭代时 foreach 的变量接收规则,导致现场写不出可运行代码。
面试官期待你在 30 秒内写出可执行片段,并解释内存优势、执行顺序、以及与传统数组返回的区别;如果还能补充 send()/current()/key() 等协程场景,基本就稳了。
知识点
- 生成器语法:
yield暂停执行并返回一个Generator对象;yield $key => $value把键值对同时送进生成器。 foreach迭代规则:
foreach ($gen as $k => $v)会依次把yield语句左侧的键、值赋给$k与$v;如果yield未显式给键,PHP 自动按 0,1,2… 补全。Generator类内置接口:实现Iterator,对外暴露key()/current()/next()/rewind(),因此可以被foreach驱动。- 内存与性能:生成器只在需要时产生数据,常驻内存仅保存当前状态,适合大文件、大表游标、无限序列等场景。
- 与
array_combine、iterator_to_array的区别:后者会一次性物化到内存,失去“惰性”优势。 - 协程进阶:
yield双向通信,send($value)把值回传给生成器内部作为yield表达式结果,可用于实现轻量协程调度。
答案
<?php
/**
* 从数据库游标里惰性返回 uid => name
*/
function userGenerator(PDO $pdo): Generator
{
$sql = 'SELECT uid, name FROM user WHERE status = 1';
$stmt = $pdo->query($sql, PDO::FETCH_ASSOC);
foreach ($stmt as $row) {
// 把 uid 作为键,name 作为值
yield $row['uid'] => $row['name'];
}
}
/* ---------- 调用端 ---------- */
$pdo = new PDO('mysql:host=127.0.0.1;dbname=demo;charset=utf8mb4', 'root', '');
foreach (userGenerator($pdo) as $uid => $name) {
// 同时拿到键和值
echo "uid={$uid}, name={$name}\n";
}
关键点:
yield $key => $value语法一次性指定键值;foreach的$uid对应键,$name对应值;- 数据流是“拉模式”,每循环一次才向 MySQL 取一行,内存占用 O(1)。
拓展思考
-
如果生成器内部需要“接收”外部指令,可用
send():$gen = (function (): Generator { $id = yield 1 => 'A'; // 外部 send 进来的值会赋给 $id yield $id => 'B'; })(); $gen->current(); // 1 => A $gen->send(99); // 把 99 回传,生成器继续运行,返回 99 => B这种双向通信在实现轻量协程调度器、Worker 消费队列时非常有用。
-
当需要一次性拿到完整数组但又不想失去复用性,可用
iterator_to_array($gen, true);第二个参数true表示保留生成器提供的键,避免自动重索引。 -
在 Laravel 中,结合
lazy()集合方法,可把生成器包装成LazyCollection,再链式调用map/filter/reduce,既保持惰性,又享受集合式 API。 -
面试陷阱题:如果
yield写成了yield array($k, $v),迭代时foreach ($gen as list($k, $v))也能解包,但这样键值语义丢失,且无法利用key()方法,面试官会认为你对生成器理解不到位。务必优先使用yield $k => $v。