在 Ruby 中的 Enumerable 模块 ( 上 ) 文章中,我们讲解了 Enumerable 模块的一些基础知识和一些基础的用法。那些未尽事宜,只能留在这个章节来讲解了。
本章节,我们主要介绍以下几个知识点
Enumerator::Generator类Enumerator::Yielder类Enumerator::Lazy类
Enumerator::Generator 类
实例化新枚举器的另一种方法是使用 Enumerator::new 方法
enumerator = Enumerator.new do |yielder| yielder.yield 21 yielder << 42 end p enumerator # => #<Enumerator: #<Enumerator::Generator:0x01>:each> res = enumerator.map { |n| n * 2 } p res # => [42, 84]
上面这个示例中,Enumerator::new 方法返回一个枚举器实例,其中,使用 Enumerator::Generator 实例作为数据源,each 方法则作为默认数据消费者
其实,Ruby 内置的 Enumerator::Generator 类不应该由开发人员直接操作。这是一个内部类,其实例有一个非常特殊的任务
这个任务就是保存传递给 Enumerator::new 方法的块 - 作为 Proc - 在 Enumerator 类的返回实例中。
然后 Ruby 将执行此 Proc 来构建提供给数据消费者方法的数据源 - 前一个示例中 #map 块的参数 n
需要注意的是,Enumerator::Generator 包含了 Enumerable 模块。因此,它实现了自己的 each 方法
现在,我们把注意力移到 Enumerator::new 块的 yielder 参数上
Enumerator::Yielder 类
yielder 参数是内置的 Enumerator::Yielder 类的一个实例
该类负责使用 Enumerator::Yielder#yield 和 Enumerator::Yielder#<< 方法构建和提供数据源。
每次调用这些方法时,都会动态构建和提供数据源
e = Enumerator.new do |yielder| yielder << 1 yielder << 2 end e.map { |n| n * 2 } # => [2, 4]
让我们一步一步地描述在 e.map 迭代期间发生的事情。
- 第一次迭代期间,yielder 将值
1作为传递给e.map方法调用的块的n参数 - 然后,yielder 产生值 2 作为传递给 e.map 方法调用的块的 n 参数
由此,数据就是这样被创建并提供给 map 枚举的每次迭代
Enumerator::Yielder#yield和Enumerator::Yielder#<<方法之间的唯一区别是,前者返回 nil,后者返回 self
Enumerator::Lazy 类 惰性枚举类
当我们链接多个枚举时会发生什么?
(1..100).map {|n| n * 2}.first(10)
上面的示例中,first(10) 枚举将会在 map {|n| n * 2} 枚举返回的集合之前开始枚举
也就是说,总共会发生 ~110 枚举,其中的 10 次用于 first(10) ,剩下的 100 则由 map 方法枚举产生
从表面上看,这太可怕了,因为我们实际想要的就是就是先执行 map 然后再执行 first ,也就是总共 100 迭代
那么如何解决这个性能问题呢?
答案是使用 Enumerator::Lazy 类
(1..100).lazy.map {|n| n * 2}.first(10)
Enumerable#lazy 方法返回 Enumerator::Lazy 类的实例
该类包含 Enumerable 模块中,且重新定义了几乎所有方法
在详细讨论惰性枚举的概念之前,让我们先看看以下基准测试
require 'benchmark/ips' Benchmark.ips do |x| x.config(:time => 5, :warmup => 2) x.report("Enumerations") do (1..100_000).map {|n| n * 2}.first(10) end x.report("Lazy Enumerations") do (1..100_000).lazy.map {|n| n * 2}.first(10) end x.compare! end
输出结果如下
$> ruby benchmark.rb
Warming up --------------------------------------
Enumerations 16.000 i/100ms
Lazy Enumerations 13.679k i/100ms
Calculating -------------------------------------
Enumerations 162.247 (± 1.2%) i/s - 816.000 in 5.030263s
Lazy Enumerations 142.999k (± 0.8%) i/s - 724.987k in 5.070187s
Comparison:
Lazy Enumerations: 142999.2 i/s
Enumerations: 162.2 i/s - 881.37x slower
我们可以看到,使用惰性枚举而不是一般的枚举可以大大提高速度
那么,Enumerator::Lazy 如何工作? 又是如何提高运行速度的 ?
(1..100_000).lazy.map {|n| n * 2}.first(10)
Enumerable#lazy 方法返回的 Enumerator::Lazy 实例调用了 Enumerator::Lazy#map 方法。
这种方法 - 几乎是这个类的所有方法 - 以特定的方式起作用。每次迭代都遵循以下执行流程:
- 获取 1..100 数据源的第一个值
1并将它传递给{| n | n * 2}块。 - 获取块的返回值,并通过
yielder将其传递给下一个枚举first(10)
这种机制称为 「 枚举链 」
但是为什么这会让 Ruby 避免遍历整个集合?
因为在惰性枚举中,最终的枚举 - 在我们的例子中是 first(10) - 负责控制枚举运行的时间。
因此,它足够聪明地说:「 我已经有了足够的数据。所以我们可以停止枚举链 」