ruby枚举器:是什么,为什么,怎么做

158 阅读5分钟

什么是枚举者?

本节将让你对几个重要的术语及其区别有一个大致的了解。在我们开始使用它们之前,我们需要理解枚举的概念和Ruby为我们提供的枚举工具。另外,我们将假设你对Ruby中的块很熟悉。

枚举可枚举模块和枚举器

枚举。作为一个概念,仅仅意味着根据一些逻辑来遍历一个项目的列表。在编程中,我们经常遇到列表,需要遍历这些列表是一个常见的编程需要。

枚举器。当使用列表时,我们通常使用for 循环或.each 方法来迭代其中的项目。我们知道for 循环是一种流程控制的形式,但是这个.each 方法是怎么来的呢?你可能认为它来自列表所属的,但事实上,它是一个继承自 *Enumerable*模块的继承方法。

这个模块,当包含在一个包含元素集的中时,允许任何一个继承一些遍历元素集所需的方法。 在Ruby中,有许多内置的枚举对象,如ArrayHashRange,所有这些都是通过使用 *Enumerable*模块。

这个模块依赖于一个叫做.each 的方法,这个方法需要在任何包含它的中实现。其他重要的方法,如.map,.reduce.select ,依靠.each 方法的实现来发挥作用,然后可以免费使用。

当在一个数组上调用一个块时,.each 方法将为数组的每个元素执行该块。

nums = [1,2,3] # An Array is an Enumerable
nums.each { |i| puts "* #{i}" }
# => 1
# => 2
# => 3

枚举者。一个通过定义Enumerator.new 或调用Enumerable 对象的实例方法来实例化的类。因此,如果我们在一个数组上调用.each 方法,而不传给它的每个元素执行一个块,我们将收到一个Enumerator 的实例。 听起来有点混乱,我知道。让我们用一个基本的例子来简化一下。

nums = [1,2,3]
puts nums.each
# => <Enumerator: 0x00007fa3657f90f8>

为什么它是好的

我们之前讨论过,for 循环也可以用来迭代一个列表。那么为什么我们需要使用Enumerable 模块提供的方法呢?嗯,你看,当我们使用for 循环时,我们有可能在我们的代码中引入一个错误,即无意中覆盖了一个先前声明的变量的值。

下面是一个例子。

fighter = 'Jackie'
fighters_list = ['Bruce', 'Rocky', 'Rambo']

for fighter in fighters_list
  puts fighter
end

puts "Your Fighter has changed => " + fighter # Unintended change

控制台输出。

=> Bruce
=> Rocky
=> Rambo
=> Your Fighter has changed =>  Rambo

很明显,这是不可取的。这就是Enumerable#each ,它可以很方便地发挥作用。如果我们使用下面的代码,*"fighter "*变量的值将保持不变。

fighters_list.each { |fighter| puts fighter }

这就是为什么使用Enumerable提供的.each 方法来迭代它几乎总是更好。我们还希望对列表做很多其他的事情,比如把一个列表减少到一个单一的值,比如它的总和,修改整个列表,如果我们想在客户端保护一些数据,还可以编写我们自己的自定义逻辑来迭代一个列表。如果我们使用Enumerable ,我们可以做到所有这些,甚至更多。我们将在稍后详细讨论用法。

如何使用枚举表?

那么我们最终如何在我们的代码中使用Enumerable呢?你在这方面有很大的自由度,有很多地方使用enumerables是有用的。

链式枚举

你可以使用.each方法来遍历列表,但是如果我们想修改列表,而我们的映射逻辑使用每个元素的索引,那该怎么办。Enumerable#map 方法似乎是一个很好的初步想法。然而,这可以修改列表,但不能跟踪单个元素的索引。

nums_enum = [1, 2, 3].map
nums_enum.each { |num| puts num }
# => 1
# => 2
# => 3

我们也有Enumerable#each_with_index 方法。这不会修改列表,但我们至少可以用它来跟踪索引。你已经可以看出我的想法了。是的,在Enumerators 的帮助下,我们可以把这两个方法连在一起,创建我们自己的 "带索引的地图"--类似于函数调用!下面是一个例子。

nums_enum = [1, 2, 3].map # Called without a block so returns #Enumerator
# => #<Enumerator: ...>  

p nums_enum.each_with_index{ |n, i| n * i } # Called with block so will iterate with index
# => [0, 2, 6]

p [1,2,3].map.each_with_index{ |n, i| n * i } # Shortened to a single line
# => [0, 2, 6]

你可以用这个方法来玩,只要能达到你的目的,就可以把大量的方法串起来。

10.times.reverse_each.cycle.first(11)

=> [9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 9]

手动迭代

在某些情况下,你可能想对你的迭代进行手动控制。Enumerable模块提供了.next 方法,你也可以在一个Enumerator上调用这个方法。

nums_enum = [1, 2].each
nums_enum.next
# => 1

nums_enum.next
# => 2

nums_enum.next
# => `next': iteration reached an end (StopIteration)

自定义类和迭代

在Ruby中,枚举的魅力在于它提供了大量的自定义功能。如果你想为某个列表或类定制一半的遍历逻辑,该怎么办?

1.枚举器

digits = (1..10).to_a # Range to Array

def odd_digits(digits)
  index = -1
  Enumerator.new do |yielder|
    loop do
      index += 1
      yielder << digits[index] if digits[index] % 2 == 0
    end
  end
end

控制台。

$> puts odd_digits(digits).first(4)
=> 2
=> 4
=> 6
=> 8

2. Enumerable 实施

到目前为止,最酷和最详细的实现用例是当你有一个包含一组元素的自定义类。正如前面所讨论的,我们需要在中添加两样东西来赋予它枚举的权力。首先,我们需要包括Enumerable 模块。其次,我们需要在该类中实现each 方法来迭代元素。大多数情况下,我们可以返回到另一个对象(如数组)的each 方法。

让我们实现一个具有自定义结构的链表,这样它就不能依赖Array#each 方法来迭代节点。

class LinkedList
  
  def initialize(head, tail = nil)
    @head, @tail = head, tail
  end

  def add(item)
    LinkedList.new(item, self)
  end

end

LinkedList.new(0).add(5).add(10)
# => <LinkedList:0x1 @head=10, @tail=<LinkedList:0x2 @head=5, @tail=<LinkedList:0x3 @head=0, @tail=nil>>>

我们已经创建了我们的链表,但是没有办法迭代各个节点/元素。为了做到这一点,我们最终访问了最后一段可能也是最重要的一段代码。

class LinkedList include Enumerable # We inherit enumerable methods that use .each

  def initialize(head, tail = nil)
    @head, @tail = head, tail
  end

  def add(item)
    LinkedList.new(item, self)
  end

  def each(&block) # Implement our custom each

    if block_given?
      block.call(@head)
      @tail.each(&block) if @tail
    else
      to_enum(:each) # Return enumerator if block not provided
    end

  end

end

那么这段代码是做什么的呢?在这一点上,Enumerable 的加入是不言自明的。更重要的是,我们实现了.each 方法,让其他可枚举方法知道如何迭代我们的链表。

.each 方法中,如果给出了一个块,我们只需在当前节点上调用该块,并在列表的其余部分递归调用*.each*,直到最后一个节点被nil 。如果没有提供块,则返回一个使用我们*.each方法的枚举器*。这最后一点将允许在前面的用例中讨论的方法链。

控制台。

$> linked_list = LinkedList.new(1).add(5).add(10)

$> linked_list.each{ |node| puts node }
=> 0
=> 5
=> 10

$> linked_list.select{ |node| node % 5 == 0 } # Select if divisible by 5
=> [10, 5]

祝贺你!我们的链接列表实现现在可以使用我们的链接列表实现现在可以访问其他有用的方法,如.map.select ,如上面的例子所示。

希望所有这些都有意义。如果有些东西还不清楚,那么可以试着通过创建你自己的枚举器并在你的代码中使用它们来练习。

谢谢你的阅读。就这样结束了