生成器返回键值对时如何同时迭代 key 与 value?

解读

国内一线/二线公司面试中,这道题常被放在“语言特性”环节,用来快速判断候选人是否真正用过生成器、是否理解 yield 的底层语义。
很多候选人只写过 yield $value,看到 yield $key => $value 会愣住;或者知道语法却说不清迭代时 foreach 的变量接收规则,导致现场写不出可运行代码。
面试官期待你在 30 秒内写出可执行片段,并解释内存优势、执行顺序、以及与传统数组返回的区别;如果还能补充 send()/current()/key() 等协程场景,基本就稳了。

知识点

  1. 生成器语法:yield 暂停执行并返回一个 Generator 对象;yield $key => $value 把键值对同时送进生成器。
  2. foreach 迭代规则:
    foreach ($gen as $k => $v) 会依次把 yield 语句左侧的键、值赋给 $k$v;如果 yield 未显式给键,PHP 自动按 0,1,2… 补全。
  3. Generator 类内置接口:实现 Iterator,对外暴露 key()/current()/next()/rewind(),因此可以被 foreach 驱动。
  4. 内存与性能:生成器只在需要时产生数据,常驻内存仅保存当前状态,适合大文件、大表游标、无限序列等场景。
  5. array_combineiterator_to_array 的区别:后者会一次性物化到内存,失去“惰性”优势。
  6. 协程进阶: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";
}

关键点:

  1. yield $key => $value 语法一次性指定键值;
  2. foreach$uid 对应键,$name 对应值;
  3. 数据流是“拉模式”,每循环一次才向 MySQL 取一行,内存占用 O(1)。

拓展思考

  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 消费队列时非常有用。

  2. 当需要一次性拿到完整数组但又不想失去复用性,可用 iterator_to_array($gen, true);第二个参数 true 表示保留生成器提供的键,避免自动重索引。

  3. 在 Laravel 中,结合 lazy() 集合方法,可把生成器包装成 LazyCollection,再链式调用 map/filter/reduce,既保持惰性,又享受集合式 API。

  4. 面试陷阱题:如果 yield 写成了 yield array($k, $v),迭代时 foreach ($gen as list($k, $v)) 也能解包,但这样键值语义丢失,且无法利用 key() 方法,面试官会认为你对生成器理解不到位。务必优先使用 yield $k => $v