连续好几天都是 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
方法用于光标移动到上一个位置