Ruby Enumerator 与 Enumerable 详解:惰性求值与迭代的艺术

Enumerable 是 Ruby 集合操作的基石,Enumerator 则带来惰性求值与手动迭代控制。本文讲清两者原理、链式调用、无限序列的写法,以及如何为自定义类实现 Enumerable。

最佳实践
红宝石与应用开发工作台插画

Enumerable 是一个模块,任何类只要定义了 each 并 include 它,就免费获得 map/select/reduce 等几十种集合方法;Enumerator 是控制迭代过程的对象,支持惰性(lazy)求值——处理大文件和无限序列时靠它避免内存爆炸。

Enumerable 的本质:each 一个就够了

Ruby 的集合方法并非数组专利。map、select、reject、reduce、sort_by、group_by……这些全部来自 Enumerable 模块,而它们内部只调用一个方法:each。

这意味着一个反直觉的事实:Hash、Range、Set、甚至文件行都能用同一套集合方法,因为它们都 include 了 Enumerable。

(1..10).select(&:even?)                    # Range
{ a: 1, b: 2 }.map { |k, v| [k, v * 2] }   # Hash

为自定义类实现 Enumerable

class Playlist
  include Enumerable

  def initialize(songs)
    @songs = songs
  end

  def each(&block)
    @songs.each(&block)
  end
end

Playlist.new(%w[a b c]).map(&:upcase)  # => ["A", "B", "C"]

只实现 each,二十多个方法自动可用。这是 Ruby 模块设计最优雅的示范。

Enumerator:把迭代握在手里

调用不带块的 each(或 map 等)会返回一个 Enumerator,迭代权交到你手上:

e = [1, 2, 3].each
e.next  # => 1
e.next  # => 2
e.next  # => 3
e.next  # => StopIteration 异常

典型用途是并行遍历两个长度不一的集合,或实现"外部迭代"——比如逐条喂给状态机。

Enumerator.new 还能从零构造序列:

fib = Enumerator.new do |y|
  a, b = 0, 1
  loop do
    y << a
    a, b = b, a + b
  end
end

fib.take(10)  # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

lazy:处理大数据的救命稻草

直接 File.read_lines.map {...}.select {...} 会把整个文件读进内存。加一行 .lazy 就完全不同:

File.foreach('huge.log')
    .lazy
    .map { |line| JSON.parse(line) rescue nil }
    .compact
    .select { |r| r['level'] == 'error' }
    .first(100)

惰性枚举器把整条链变成"逐元素流过",需要 100 条就只处理到 100 条为止,内存占用恒定。无限序列(如上面的斐波那契)之所以能 take(10),也是因为 take 触发的是惰性语义。

链式枚举的工程实践

实际代码里常见的链式写法:collection.filter_map { ... }.group_by { ... }.transform_values { ... }。几点建议:

  • filter_map(Ruby 2.7+)合并 map+compact,少一次遍历;
  • 多层链式中间产生的临时数组在大数据量下可观,数据超过万级就考虑 lazy 或数据库侧处理;
  • each_with_object 比 reduce 更适合构建 Hash/数组结果。

常见问题(FAQ)

Q:Enumerable 和 Enumerator 是一回事吗?

不是。Enumerable 是模块(能力的来源),Enumerator 是类(迭代过程的对象化)。前者回答"能做什么",后者回答"做到哪一步了"。

Q:什么时候必须用 lazy?

两个信号:数据量大到不想全量进内存(日志文件、导出任务),或数据源是无限/未知长度的(流式 API、实时事件)。小数组上加 lazy 反而更慢,别过度使用。

Q:each 和 map 性能差多少?

map 会构建新数组,增加与结果数量相关的内存开销;只要副作用(写库、发请求)时用 each。链式中间环节产生的数组才是优化重点,必要时用 lazy 或重构成单次遍历。


参考文档

本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。

延伸阅读

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台