为什么`.each`在`.select`之后调用惰性枚举时返回nil?

时间:2017-04-19 15:19:52

标签: ruby enums

我有一段代码如下:

sent_messages = messages.lazy.reject { |m| message_is_spam?(m) }
                             .each   { |m| send_message(m) }
# Do something with sent_messages...

某些上下文:如果邮件的收件人在最近5分钟内收到消息,则message_is_spam?方法返回true。当messages包含同一收件人的多条邮件时,后一条邮件仅在发送第一条邮件后才会被视为垃圾邮件。为了确保后一条消息被视为垃圾邮件,我懒惰地拒绝垃圾邮件并发送它们。

我希望.each返回包含所有项目的数组,但我得到nil.each总是返回一个数组,除了在这一个场景中:

[].each {}                # => []
[].lazy.each {}           # => []
[].select {}.each {}      # => []
[].lazy.select {}.each {} # => nil

为了增加混淆,JRuby在上面的所有示例中都返回[]

为什么.each在这样调用时返回nil?我在文档中找不到任何关于它的内容,很难弄清楚C代码中发生了什么。

我已经找到了彻底绕过这个问题的方法;如果我为每个收件人选择最多1封邮件(messages.uniq_by(&:recipient)),则该操作不再需要 。尽管如此,这仍然让我感到惊讶。

2 个答案:

答案 0 :(得分:3)

可能的解释

Enumerator::Lazy的目的之一是避免在内存中有一个巨大的(或可能是无限的)数组。这可以解释为什么Enumerator#each不返回所需的数组。

Lazy#reject这样的方法更倾向于返回nil作为替代值(之后由each返回的值),而不是冒着使用大型阵列耗尽内存的风险:

return lazy_add_method(obj, 0, 0, Qnil, Qnil, &lazy_reject_funcs);

相比之下,Enumerable#lazy会返回:

VALUE result = lazy_to_enum_i(obj, sym_each, 0, 0, lazyenum_size);

我怀疑有不同的论点:

    {li> Qnil reject {li> sym_each lazy

原因是:

  • [].lazy.each {}返回[]
  • [].lazy.select{}.each {}返回nil

each返回数组或nil似乎不一致

替代

每个

代码的更详细的替代方法可能是:

messages = %w(a b c)
messages_to_send = messages.lazy.reject{|x| puts "Is '#{x}' spam?"}
messages_to_send.each{ |m| puts "Send '#{m}'" }
# Is 'a' spam?
# Send 'a'
# Is 'b' spam?
# Send 'b'
# Is 'c' spam?
# Send 'c'

Lazy#reject会返回Lazy枚举器,因此第二个message_is_spam?将在第一个send_message之后执行。

但是有一个问题,在懒惰的枚举器上调用to_a会再次调用reject

sent_messages = messages_to_send.to_a
# Is 'a' spam?
# Is 'b' spam?
# Is 'c' spam?

map和修改后的方法

您还可以在m结束时返回send_message并使用Lazy#map

sent_messages = messages.lazy.reject { |m| message_is_spam?(m) }
                             .map { |m| send_message(m) }.to_a

map应该可靠地返回所需的Enumerator :: Lazy对象。调用Enumerable#to_a可确保sent_messages为数组。

map并明确返回

如果您不想修改send_message,则可以在每次m次迭代结束时明确返回map

messages = %w(a b c)

sent_messages = messages.lazy.reject{ |m| puts "Is '#{m}' spam?" }
                             .map{ |m| puts "Send '#{m}'"; m }.to_a   
# Is 'a' spam?
# Send 'a'
# Is 'b' spam?
# Send 'b'
# Is 'c' spam?
# Send 'c'

p sent_messages
# ["a", "b", "c"]

修改逻辑

另一种选择是在没有lazy的情况下重新定义逻辑:

sent_messages = messages.map do |m|
  next if message_is_spam?(m)
  send_message(m)
  m
end.compact

答案 1 :(得分:0)

如果使用.map,则返回预期结果,在这种情况下每个给出nil的原因都不清楚。

 p [1,2,3].lazy.select{|x| x>1 }.map{|x| x}.to_a #=> [2, 3]

或只是

p [1,2,3].lazy.select{|x| x>1 }.to_a #=> [2, 3]

关于此主题,请阅读此http://railsware.com/blog/2012/03/13/ruby-2-0-enumerablelazy/博客