当你的类没有定义 #each 时,返回 Enumerator::Lazy 的最佳方法是啥?

Posted

技术标签:

【中文标题】当你的类没有定义 #each 时,返回 Enumerator::Lazy 的最佳方法是啥?【英文标题】:What's the best way to return an Enumerator::Lazy when your class doesn't define #each?当你的类没有定义 #each 时,返回 Enumerator::Lazy 的最佳方法是什么? 【发布时间】:2013-04-04 09:30:18 【问题描述】:

Enumerable#lazy 依赖于您的可枚举提供 #each 方法。如果你的枚举没有#each 方法,你就不能使用#lazy。现在Kernel#enum_for#to_enum 提供了指定#each 以外的枚举方法的灵活性:

Kernel#enum_for(method = :each, *args)

但是#enum_for 和朋友们总是构造普通的(非惰性的)枚举器,而不是Enumerator::Lazy

我看到 Ruby 1.9.3 中的 Enumerator 提供了这种类似的 #new 形式:

Enumerator#new(obj, method = :each, *args)

不幸的是,构造函数在 Ruby 2.0 中已被完全删除。另外我认为它在Enumerator::Lazy 上根本不可用。所以在我看来,如果我有一个带有方法的类,我想为其返回一个惰性枚举器,如果该类没有 #each,那么我必须定义一些确实定义了 #each 的辅助类。

例如,我有一个 Calendar 类。对我来说,从一开始就列举每个日期是没有意义的。 #each 将毫无用处。相反,我提供了一种从开始日期(懒惰地)枚举的方法:

  class Calendar
    ...
    def each_from(first)
      if block_given?
        loop do
          yield first if include?(first)
          first += step
        end
      else
        EachFrom.new(self, first).lazy
      end
    end
  end

EachFrom 类看起来像这样:

class EachFrom
  include Enumerable
  def initialize(cal, first)
    @cal   = cal
    @first = first
  end
  def each
    @cal.each_from(@first) do |yielder, *vals|
      yield yielder, *vals
    end
  end
end

它有效,但感觉很重。也许我应该继承Enumerator::Lazy 并定义一个类似Enumerator 中已弃用的构造函数。你怎么看?

【问题讨论】:

【参考方案1】:

我认为你应该使用to_enum返回一个普通的Enumerator

class Calendar
  # ...
  def each_from(first)
    return to_enum(:each_from, first) unless block_given?
    loop do
      yield first if include?(first)
      first += step
    end
  end
end

这是大多数红宝石学家所期望的。即使是无限的Enumerable,它仍然可以使用,例如:

Calendar.new.each_from(1.year.from_now).first(10) # => [...first ten dates...]

如果他们真的需要一个惰性枚举器,他们可以自己调用lazy

Calendar.new.each_from(1.year.from_now)
  .lazy
  .map...
  .take_while...

如果你真的想要返回一个惰性枚举器,你可以从你的方法中调用lazy

  # ...
  def each_from(first)
    return to_enum(:each_from, first).lazy unless block_given?
    #...

但我不推荐它,因为它会出乎意料 (IMO),可能会过度杀伤并且性能会降低。

最后,你的问题有几个误解:

Enumerable 的所有方法都假定each,而不仅仅是lazy

您可以定义一个each 方法,如果您愿意,该方法需要一个参数并包括EnumerableEnumerable 的大多数方法都不起作用,但 each_with_index 和其他几个方法会转发参数,以便立即使用。

没有块的Enumerator.new 消失了,因为应该使用to_enum。请注意,块形式仍然存在。 Lazy 也有一个构造函数,但它是从现有的 Enumerable 开始的。

您声明 to_enum 永远不会创建惰性枚举器,但这并不完全正确。 Enumerator::Lazy#to_enum 专门用于返回惰性枚举器。 Enumerable 上调用 to_enum 的任何用户方法都将使惰性枚举器保持惰性。

【讨论】:

你让我大吃一惊 Marc-André。我的代码刚刚从白痴变成了惯用语。我不明白 Ruby 希望我们始终使用 Enumerators 而不是 Enumerator::Lazy。每当我们需要一些懒惰的东西时,我们都会向枚举器询问#lazy 版本。缺点可能是我们抽象的用户确实必须了解何时调用#lazy(例如,在调用#drop(n) 之前)。好处是代码清晰。 我在#drop(n) 方面遇到了很多麻烦(并且从中学到了很多)。现在我要在所有地方返回“普通”枚举器,我不得不洒一些 ...lazy.drop(n)... 。所以我定义了一个 drop-like 方法,它简单地推进 Enumerator,让我将它们更改为 ...skip(n)... 对。您绝对可以定义一个像 Enumerable#skip(n) 这样的方法,它会返回一个 Enumerator 而不是像 drop 这样的数组并使用它。 在上面的第三个项目符号中,您说“Enumerator 构造函数没有消失......”我指出非块形式已经消失(允许您指定除 #each 之外的枚举方法的形式)。再次感谢您向我介绍 Marc-André!

以上是关于当你的类没有定义 #each 时,返回 Enumerator::Lazy 的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发:UINavigationController自定义返回按钮,系统导航支持侧滑返回

春哥博客

对于不实现 Iterable 的类,如何使用 for-each 循环

“EACH”关键字在“GROUP EACH BY”中没有返回正确的结果

当你的才华还撑不起你的野心时,就应该静下心来学习

使用 jQuery.each() 时返回一个值?