Ruby中Enumerator类的用途是什么?

如果我像这样创建一个Enumertor:

enum = [1,2,3].each => # 

enum是一个枚举器。 这个对象的目的是什么? 我不能这样说:

 enum { |i| puts i } 

但我可以这样说:

 enum.each { |i| puts i } 

这似乎是多余的,因为Enumerator是用.each创建的。 它似乎存储了有关each方法的一些数据。

我不明白这里发生了什么。 我确信我们有这个Enumerator类有一些合乎逻辑的原因,但是它能做什么,Array不能呢? 我想也许它是Array和其他Enumerables的祖先,但它似乎不是。 究竟是什么原因导致Enumerator类存在,以及它将在何种上下文中使用?

如果你做enum = [1,2,3].each; enum.next会发生什么? enum = [1,2,3].each; enum.next

 enum = [1,2,3].each => # enum.next => 1 enum.next => 2 enum.next => 3 enum.next StopIteration: iteration reached an end 

当您有一个执行计算的枚举器(如素数计算器或Fibonacci序列生成器)时,这可能很有用。 它为您编写代码提供了灵活性。

我认为,主要目的是按需求获取元素,而不是将它们全部放在一个循环中。 我的意思是这样的:

 e = [1, 2, 3].each ... do stuff ... first = e.next ... do stuff with first ... second = e.next ... do more stuff with second ... 

请注意,那些do stuff部件可以在远离彼此的不同function中。

懒惰地评估无限序列(例如素数,斐波纳契数,字符串键,如'a'..'z','aa'..'az','ba'..'zz','aaa'..等等)调查员的一个很好的用例。

到目前为止,当您想要迭代可能无限长度的数据序列时,Enumerator会派上用场。

以一个扩展Enumerator的素数生成器prime_generator为例。 如果我们想获得前5个素数,我们可以简单地编写prime_generator.take 5而不是将“limit”嵌入到生成逻辑中。 因此,我们可以分离生成素数并从生成的素数中取一定数量,使得生成器可重复使用。

我喜欢使用Enumerable返回枚举器方法的方法链接,如下例所示(它可能不是“目的”,但我想指出它的美学方面):

 prime_generator.take_while{|p| p < n}.each_cons(2).find_all{|pair| pair[1] - pair[0] == 2} 

这里的prime_generator是Enumerator的一个实例,它逐个返回素数。 我们可以使用Enumerable的take_while方法获取n以下的素数。 each_consfind_all方法都返回Enumerator,因此可以链接它们。 此示例旨在生成n以下的孪生素数。 这可能不是一种有效的实现,但很容易在一行中编写,并且适用于原型设计的恕我直言。

这是一个基于Enumerator的非常简单的prime_generator实现:

 def prime?(n) n == 2 or (n >= 3 and n.odd? and (3...n).step(2).all?{|k| n%k != 0}) end prime_generator = Enumerator.new do |yielder| n = 1 while true yielder << n if prime? n n += 1 end end 

可以组合枚举器:

 array.each.with_index { |el, idx| ... } 

要了解枚举器类的主要优点,首先需要区分内部和外部迭代器。 使用内部迭代器,迭代器本身控制迭代。 使用外部迭代器,客户端(通常是程序员)控制迭代。 使用外部迭代器的客户端必须推进遍历并从迭代器显式请求下一个元素。 相反,客户端向内部迭代器提供要执行的操作,迭代器将该操作应用于集合中的每个元素。

在Ruby中,Enumerator类使您可以使用外部迭代器。 一旦了解了外部迭代器,您将开始发现许多优点。 首先,让我们看看Enumerator类如何促进外部迭代:

 class Fruit def initialize @kinds = %w(apple orange pear banana) end def kinds yield @kinds.shift yield @kinds.shift yield @kinds.shift yield @kinds.shift end end f = Fruit.new enum = f.to_enum(:kinds) enum.next => "apple" f.instance_variable_get :@kinds => ["orange", "pear", "banana"] enum.next => "orange" f.instance_variable_get :@kinds => ["pear", "banana"] enum.next => "pear" f.instance_variable_get :@kinds => ["banana"] enum.next => "banana" f.instance_variable_get :@kinds => [] enum.next StopIteration: iteration reached an end 

重要的是要注意,在对象上调用to_enum并传递与方法对应的符号将实例化Enumerator类,在我们的示例中,枚举局部变量包含Enumerator实例。 然后我们使用外部迭代遍历我们创建的枚举方法。 我们的枚举方法称为“种类”,并注意我们使用yield方法,我们通常使用块。 这里,枚举器将一次产生一个值。 它在每次收益后暂停。 当被要求提供另一个值时,它将在最后一个产生的值之后立即恢复,并执行到下一个产生的值。 当没有任何东西可以产生,并且你调用next时,它将调用StopIterationexception。

那么Ruby中外部迭代的力量是什么? 有几个好处,我将重点介绍其中的一些。 首先,Enumerator类允许链接。 例如,with_index在Enumerator类中定义,它允许我们在迭代Enumerator对象时指定迭代的起始值:

 f.instance_variable_set :@kinds, %w(apple orange pear banana) enum.rewind enum.with_index(1) do |name, i| puts "#{name}: #{i}" end apple: 1 orange: 2 pear: 3 banana: 4 

其次,它从Enumerable模块中提供了一些有用的便利方法。 记住Enumerator是一个类,Enumerable是一个模块,但Enumerable模块包含在Enumerator类中,因此Enumerators是Enumerable:

 Enumerator.ancestors => [Enumerator, Enumerable, Object, Kernel, BasicObject] f.instance_variable_set :@kinds, %w(apple orange pear banana) enum.rewind enum.detect {|kind| kind =~ /^a/} => "apple" enum => #:kinds> 

并且Enumerator的另一个主要好处可能不会立即明确。 让我通过演示解释一下。 您可能知道,通过包含Enumerable模块并定义每个实例方法,您可以使任何用户定义的类可枚举:

 class Fruit include Enumerable attr_accessor :kinds def initialize @kinds = %w(apple orange pear banana) end def each @kinds.each { |kind| yield kind } end end 

这很酷。 现在我们可以使用大量的Enumerable实例方法,如chunkdrop_whileflat_mapgreplazypartitionreducetake_while等等。

 f.partition {|kind| kind =~ /^a/ } => [["apple"], ["orange", "pear", "banana"]] 

有趣的是,Enumerable模块的每个实例方法实际上都在后台调用我们的每个方法以获取可枚举的项目。 因此,如果我们要实现reduce方法,它可能看起来像这样:

 module Enumerable def reduce(acc) each do |value| acc = yield(acc, value) end acc end end 

注意它是如何将块传递给每个方法的,因此我们的每个方法都会产生一些回到块的东西。

但是看看如果客户端代码调用每个方法而不指定块会发生什么:

 f.each LocalJumpError: no block given (yield) 

所以现在我们可以修改我们的每个方法以使用enum_for,当没有给出块时,它将返回一个Enumerator对象:

 class Fruit include Enumerable attr_accessor :kinds def initialize @kinds = %w(apple orange pear banana) end def each return enum_for(:each) unless block_given? @kinds.each { |kind| yield kind } end end f = Fruit.new f.each => #:each> 

现在我们有了一个Enumerator实例,我们可以用我们的客户端代码控制它以供以后使用。