Ruby 中的 Enumerable 模块 ( 下 )

yufei       5 年, 8 月 前       1389

Ruby 中的 Enumerable 模块 ( 上 ) 文章中,我们讲解了 Enumerable 模块的一些基础知识和一些基础的用法。那些未尽事宜,只能留在这个章节来讲解了。

本章节,我们主要介绍以下几个知识点

  1. Enumerator::Generator
  2. Enumerator::Yielder
  3. 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#yieldEnumerator::Yielder#<< 方法构建和提供数据源。

每次调用这些方法时,都会动态构建和提供数据源

e = Enumerator.new do |yielder|
  yielder << 1
  yielder << 2
end

e.map { |n| n * 2 } # => [2, 4]

让我们一步一步地描述在 e.map 迭代期间发生的事情。

  1. 第一次迭代期间,yielder 将值 1 作为传递给 e.map 方法调用的块的 n 参数
  2. 然后,yielder 产生值 2 作为传递给 e.map 方法调用的块的 n 参数

由此,数据就是这样被创建并提供给 map 枚举的每次迭代

Enumerator::Yielder#yieldEnumerator::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. 获取 1..100 数据源的第一个值 1 并将它传递给 {| n | n * 2} 块。
  2. 获取块的返回值,并通过 yielder 将其传递给下一个枚举 first(10)

这种机制称为 「 枚举链 」

但是为什么这会让 Ruby 避免遍历整个集合?

因为在惰性枚举中,最终的枚举 - 在我们的例子中是 first(10) - 负责控制枚举运行的时间。

因此,它足够聪明地说:「 我已经有了足够的数据。所以我们可以停止枚举链 」

目前尚无回复
简单教程 = 简单教程,简单编程
简单教程 是一个关于技术和学习的地方
现在注册
已注册用户请 登入
关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.