Ruby 中的 Enumerable 模块 ( 上 )

yufei       5 年, 8 月 前       1396

连续好几天都是 Ruby 相关的知识点,我自己都有点腻了...,但这也是没办法的事,众多小语言,我就对 Ruby 不怎么熟悉,所以要多学学

本章节,我们就来讲讲 Ruby 中的枚举 ( Enumerable ) 模块,我们主要会涉及到以下几个知识点

  1. Enumerable 模块
  2. Enumerable

Enumerable 模块

Enumerable 模块为类带来了一堆方法,包括但不限于

  1. 遍历 ( Traversal ) 方法
  2. 查找 ( Search ) 方法
  3. 排序 ( Sort ) 方法

该模块广泛用于最流行的 Ruby gems 和项目中,例如 Ruby on Rails,devise 等

此外,此模块还定义了少量的几个但很重要的 Ruby 类,如 ArrayHashRange

我们来简单的阐述下 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

上面的代码中

  1. 首先,我们使用流行的 map 方法遍历每个元素,并将每个元素 +1 ,然后返回新元素组成的数组
  2. 其次,我们使用了 sort 方法对数组的元素进行排序,排序采用了 ASCII 字母排序。
  3. 最后,我们使用了查找方法 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 方法用于光标移动到上一个位置

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

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

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