连续好几天都是 Ruby 相关的知识点,我自己都有点腻了...,但这也是没办法的事,众多小语言,我就对 Ruby 不怎么熟悉,所以要多学学
本章节,我们就来讲讲 Ruby 中的枚举 ( Enumerable ) 模块,我们主要会涉及到以下几个知识点
Enumerable模块Enumerable类
Enumerable 模块
Enumerable 模块为类带来了一堆方法,包括但不限于
- 遍历 ( Traversal ) 方法
 - 查找 ( Search ) 方法
 - 排序 ( Sort ) 方法
 
该模块广泛用于最流行的 Ruby gems 和项目中,例如 Ruby on Rails,devise 等
此外,此模块还定义了少量的几个但很重要的 Ruby 类,如 Array,Hash,Range 
我们来简单的阐述下 Enumerable API,重点详细介绍遍历,排序和搜索方法
irb> [1, 2, 3].map {|n| n + 1} => [2, 3, 4] irb> %w[a l p h a b e t].sort => ["a", "a", "b", "e", "h", "l", "p", "t"] irb> [21, 42, 84].first => 21
上面的代码中
- 首先,我们使用流行的 
map方法遍历每个元素,并将每个元素 +1 ,然后返回新元素组成的数组 - 其次,我们使用了 
sort方法对数组的元素进行排序,排序采用了 ASCII 字母排序。 - 最后,我们使用了查找方法 
first返回数组的第一个元素。 
包含 Enumerable 模块
使用 include Enumerable 包含了 Enumerable 的类都必须实现 each 方法
class Users include Enumerable def initialize @users = %w[John Mehdi Henry] end end irb> Users.new.map { |user| user.upcase } NoMethodError: undefined method `each' for <Users:000fff>
上面的示例中,之所以会引发 NoMethodError 错误,是因为 Enumerable#map 的内部实现中,调用了 Users 类的 each 方法,很显然,这个方法是未定义的。
那么,我们就给 Users 类添加上该方法呗
class Users include Enumerable def initialize @users = %w[John Mehdi Henry] end def each for user in @users do yield user end end end irb> Users.new.map { |user| user.upcase } => ["JOHN", "MEHDI", "HENRY"]
上面的代码中,我们在 Users 类中定义了 each 方法用于遍历 @users 数组 ( 这是 Users 类的数据源 ),然后返回 @users 数组的每个值
因为定义了 Users#each 方法,因此 Enumerable#map 方法可以使用它并将每个用户作为其块的参数
如果你不熟悉 yield 关键字,请随时阅读 yield 关键字文章 Ruby 中的 yeild 关键字 ( 上 ) 和 Ruby 中的 yeild 关键字 ( 下 )
关于 Enumerable 模块的更多信息,欢迎浏览官方文档 https://ruby-doc.org/core-2.5.1/Enumerable.html
接下来,为了更加熟悉 Enumerable 模块,我们即将深入探究 Enumerator 类。
Enumerator 类
Enumerator 类是一个数据源,既可以被 Enumerable 方法使用,也可以用于外部迭代
如何使用 Enumerator 类
irb> enumerator = [1, 2, 3].map => #<Enumerator: [1, 2, 3]:map>
就像上面代码中的那样,如果没有传递任何参数给 map ( 或几乎所有 Enumerable 模块的方法 ),则返回Enumerator 类的实例
此 enumerator 枚举器链接到 [1,2,3] 数组 ( 数据源 ) 和 map 方法 ( 数据消费者 ) 
然后我们就可以调用 enumerator.each 来完成对数据源的消费
irb> enumerator.each { |n| puts n; n + 2 } 1 2 3 => [3, 4, 5]
在上面的示例中,map 数据使用者会在 each 方法的上下文中执行。
正如你所看到的那样,枚举器 ( enumerator ) 只会执行一次块的内容 - 因为只调用了 3 次 puts 方法。
日常使用时,不要模仿这个用例,因为这个用例效率不高,我们更喜欢 map 的使用方式为 [1, 2, 3].map { |n| puts n; n + 2 } 
相比较于上面示例中的使用方式,其实还存在一个更好的 ( 但不是最好的 )
irb> %w[France Croatia Belgium].map.with_index do |c, i| "##{i + 1}: #{c}" end => ["#1: France", "#2: Croatia", "#3: Belgium"]
由于 map_with_index 并不是 Enumerable 模块的一部分,因此复制此方法行为的一种很酷的方法是使用不带参数的 map 调用返回的 Enumerator,然后调用 Enumerator#with_index 方法。这样,map 数据消费者就可以在 with_index 方法的上下文中执行
链接枚举器
当把迭代逻辑封装在方法中时,迭代称为内部迭代 ( internal ) 。例如,Users#each 方法就定义在 Include the Enumerable module 的章节
相反,当迭代逻辑在方法之外定义时,迭代被称为外部迭代 ( external )
我们来看看 Enumerator 模块如何处理外部迭代
irb> enumerator = [1,2,3].to_enum => #<Enumerator: [1, 2, 3]:each>
Kernel#to_enum 方法返回 Enumerator 类的实例,其中 self ( 在本例中为 [1,2,3] 数组 ) 作为数据源,而 each 方法则作为默认数据消费者
irb> Enumerator.instance_methods(false) => [:with_index, ..., :peek, ..., :rewind, ..., :next]
Enumerator 类提供了一组方法来操作内部游标,以保持外部迭代的状态
irb> enumerator.next => 1 irb> enumerator.peek => 1 irb> enumerator.next => 2 irb> enumerator.next => 3 irb> enumerator.next StopIteration (iteration reached an end) irb> enumerator.rewind => #<Enumerator: [1, 2, 3]:each> irb> enumerator.peek => 2
从上面的代码中可以看出,Enumerator#peek 方法返回游标位置包含的值
Enumerable#next 方法则将游标移动到下一个位置。
当游标已经处于数据源的最后位置时,再调用 Enumerator#next 将抛出 StopIteration 错误
Enumerable#rewind 方法用于光标移动到上一个位置