代表性面试主题

通用面试题:Iterator.concat 如何合并惰性数据源?

通用困难
Offer.cc 编辑团队发布 更新

题干

请实现一个合并数组、Set 与生成器的惰性迭代器,并解释 Iterator.concat 与数组 concat、Iterator.from、flatMap 的边界。

1. 题目与适用场景

日志聚合服务要按优先级依次读取内存数组、去重后的 Set 和分页生成器,消费者只需要前 100 条记录。请使用 Iterator.concat 合并这些 Iterable,保持惰性,说明无限数据源、异常和提前停止时如何释放底层迭代器。

2. 面试官考察点

  • 能否区分 Iterable 与 Iterator,并知道 Iterator.concat 要求参数实现 Iterable 协议。
  • 能否说明返回值是新的惰性 Iterator,消费时才拉取各输入,且输入按顺序连接。
  • 能否识别无限源会遮蔽其后的输入,以及参数列表必须有限且受引擎参数数量限制。
  • 能否处理 return 关闭、TypeError、兼容降级与不必要的数组物化。

3. 回答前需要澄清的问题

  1. 每个输入是可重复的 Iterable,还是已经推进过的单次 Iterator?
  2. 消费者需要流式接口,还是最终必须得到数组?
  3. 任一输入是否持有文件、网络或数据库游标,需要提前终止时关闭?
  4. 目标浏览器和 Node 版本是否原生提供 Iterator.concat,是否允许 polyfill?

4. 30 秒回答框架

我会先确认所有参数都实现 Symbol.iterator,再调用 Iterator.concat 返回一个惰性 Iterator。它按输入顺序逐个取得迭代器,只有消费者调用 nexttake 或展开时才读取数据;take(100) 后停止拉取并触发底层关闭。无限输入放在末尾,否则后续输入永远不可达;旧运行时用生成器封装同样的惰性和关闭语义。

5. 分步骤深入解答

第一步:确认 Iterable 边界

Iterator.concat 接受的是 Iterable,而不只是拥有 next 的 Iterator。数组、Set、生成器对象都可传入;只有 Iterator 的对象需要先用 Iterator.from 包装。这个约束让 concat 可以取得并负责关闭当前输入的迭代器。

第二步:建立惰性合并

js
function* pages() {
  yield { source: "page", id: 1 };
  yield { source: "page", id: 2 };
}

const memory = [{ source: "memory", id: 1 }];
const unique = new Set([{ source: "set", id: 1 }, { source: "set", id: 2 }]);
const merged = Iterator.concat(memory, unique, pages());
const firstThree = merged.take(3).toArray();

创建 merged 不会遍历任何输入;toArray 才开始消费。输出顺序是 memory、Set、pages,且不同输入中的值不会自动去重或转换。

第三步:理解消费和单次状态

返回的 Iterator 保存当前输入及其位置。一个消费者推进后,另一个消费者接手的是剩余值;需要重放时应重新取得各 Iterable 的 Iterator。若只想查看前 N 条,应在 concat 后使用 take,不要先展开成数组。

第四步:处理无限输入

任何输入可以是无限 Iterable,结果也会无限。若第一个输入无限,后续输入永远不会被读取;因此有限批次应排在前面,或在消费端设置 take、超时和取消边界。不要写 Iterator.concat(...infiniteIterables()),因为展开参数本身永远无法完成。

第五步:关闭、异常与兼容降级

当消费提前停止或抛出异常时,应让当前迭代器的 return 有机会执行,释放游标、文件句柄或网络连接。自定义迭代器可在 finally 中记录清理。缺少原生实现的运行时可用生成器实现按顺序 yield*,并补齐 return 传播;降级不能偷偷把所有输入复制到数组。

6. 高质量示范回答

我会校验参数是 Iterable,再用 Iterator.concat 创建惰性合并器,只有消费端调用 take(100)toArray 时才读取输入。concat 按顺序取得各输入迭代器,输入中的值不自动去重;无限源若位于第一个位置会让后续源永远不可达。提前停止和异常路径要验证底层 return 已关闭资源。对旧运行时,我会用生成器保留惰性、顺序、单次消费和关闭语义,避免预先物化数组。

7. 常见错误

  • 把只有 next 的对象直接传入 → 抛出 TypeError → 用 Iterator.from 包装或实现 Symbol.iterator
  • 误以为创建 concat 会立即执行 → 产生隐藏 I/O → 强调消费操作才拉取数据。
  • 把无限 Iterable 放在第一个 → 后续输入永远不可达 → 调整顺序并设置 take 或取消边界。
  • 先用展开运算符合并 → 内存和等待时间失控 → 只在明确需要数组的边界调用 toArray
  • 忽略提前终止的 return → 游标泄漏 → 用带计数的测试 Iterable 验证关闭。

8. 追问及应对

追问一:它和数组 concat 有什么区别?

数组 concat 立即处理已物化数组并返回数组;Iterator.concat 接受 Iterable,返回惰性 Iterator,可处理生成器和无限源。

追问二:为什么不能把所有 Iterator 都直接传入?

只有 Iterator 却不是 Iterable 时,concat 无法明确取得尚未访问输入的所有权和关闭责任。先用 Iterator.from 使其符合 Iterable 边界。

追问三:什么时候用 flatMap

当输入本身是大量甚至无限的 Iterable 列表时,flatMap 可以逐项产生并展开,避免一次性展开参数列表;concat 更适合有限且已知的输入集合。

追问四:如何验证资源真的关闭?

写一个实现 nextreturn 的测试 Iterable,记录拉取次数和关闭次数;对 take、消费者中断及回调抛错分别断言停止拉取并执行 return

公开来源

同类题目