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

Joe*_*oep 16 ruby enums

我有一段代码如下:

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

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

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

[].each {}                # => []
[].lazy.each {}           # => []
[].select {}.each {}      # => []
[].lazy.select {}.each {} # => nil
Run Code Online (Sandbox Code Playgroud)

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

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

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

Eri*_*nil 3

可能的解释

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

Lazy#reject类似的方法更喜欢返回nil作为替代值(之后返回的值each),而不是冒着用一个巨大的数组耗尽内存的风险:

return lazy_add_method(obj, 0, 0, Qnil, Qnil, &lazy_reject_funcs);
Run Code Online (Sandbox Code Playgroud)

相比之下,Enumerable#lazy返回:

VALUE result = lazy_to_enum_i(obj, sym_each, 0, 0, lazyenum_size);
Run Code Online (Sandbox Code Playgroud)

我怀疑不同的论点:

  • Qnil为了reject
  • 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'
Run Code Online (Sandbox Code Playgroud)

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?
Run Code Online (Sandbox Code Playgroud)

map和修改方法

您还可以m在末尾返回send_message并使用Lazy#map

sent_messages = messages.lazy.reject { |m| message_is_spam?(m) }
                             .map { |m| send_message(m) }.to_a
Run Code Online (Sandbox Code Playgroud)

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"]
Run Code Online (Sandbox Code Playgroud)

修改逻辑

另一种选择是重新定义您的逻辑,而不需要lazy

sent_messages = messages.map do |m|
  next if message_is_spam?(m)
  send_message(m)
  m
end.compact
Run Code Online (Sandbox Code Playgroud)

  • 该集合不必很大。也许“message_is_spam?”只是一个昂贵的操作,OP 希望尽快发送每条非垃圾邮件消息,而不是等待所有检查完成。顺便说一句,我认为 `sent_messages` 应该是一个数组,而不是一个 `Enumerator::Lazy` 实例。 (3认同)