如何在Ruby中映射和删除nil值


361

我有一个map更改值或将其设置为nil的方法。然后,我想从列表中删除零个条目。该列表不需要保留。

这是我目前拥有的:

# A simple example function, which returns a value or nil
def transform(n)
  rand > 0.5 ? n * 10 : nil }
end

items.map! { |x| transform(x) } # [1, 2, 3, 4, 5] => [10, nil, 30, 40, nil]
items.reject! { |x| x.nil? } # [10, nil, 30, 40, nil] => [10, 30, 40]

我知道我可以做一个循环并有条件地收集在另一个数组中,如下所示:

new_items = []
items.each do |x|
    x = transform(x)
    new_items.append(x) unless x.nil?
end
items = new_items

但这似乎不是惯用法。有没有一种很好的方法可以在列表上映射函数,从而可以在移动过程中删除/排除nil?


3
Ruby 2.7引入了filter_map,这似乎是完美的。无需重新处理数组,而无需根据需要进行第一次处理。更多信息在这里。
SRack,

Answers:


21

Ruby 2.7以上

现在有!

Ruby 2.7正是filter_map为此目的而引入的。它是惯用语言和高性能,我希望它很快会成为标准。

例如:

numbers = [1, 2, 5, 8, 10, 13]
enum.filter_map { |i| i * 2 if i.even? }
# => [4, 16, 20]

在您的情况下,由于该块的评估结果为假,因此只需:

items.filter_map { |x| process_x url }

Ruby 2.7添加了Enumerable#filter_map ”是关于该主题的不错的阅读,它针对一些较早的解决此问题的方法提供了性能基准:

N = 1_00_000
enum = 1.upto(1_000)
Benchmark.bmbm do |x|
  x.report("select + map")  { N.times { enum.select { |i| i.even? }.map{|i| i + 1} } }
  x.report("map + compact") { N.times { enum.map { |i| i + 1 if i.even? }.compact } }
  x.report("filter_map")    { N.times { enum.filter_map { |i| i + 1 if i.even? } } }
end

# Rehearsal -------------------------------------------------
# select + map    8.569651   0.051319   8.620970 (  8.632449)
# map + compact   7.392666   0.133964   7.526630 (  7.538013)
# filter_map      6.923772   0.022314   6.946086 (  6.956135)
# --------------------------------------- total: 23.093686sec
# 
#                     user     system      total        real
# select + map    8.550637   0.033190   8.583827 (  8.597627)
# map + compact   7.263667   0.131180   7.394847 (  7.405570)
# filter_map      6.761388   0.018223   6.779611 (  6.790559)

1
真好!感谢您的更新:) Ruby 2.7.0发布后,我认为将已接受的答案切换到此答案可能很有意义。我不确定这里的礼节是什么,您是否通常给现有的已接受回复一个更新的机会?我认为这是引用2.7中新方法的第一个答案,因此应成为公认的答案。@ the-tin-man你同意这个观点吗?
皮特·汉密尔顿,

感谢@PeterHamilton-感谢您的反馈,并希望它对很多人有用。我很高兴做出您的决定,尽管显然我喜欢您提出的论点:)
SRack

是的,对于具有核心团队进行倾听的语言而言,这是件好事。
锡人

建议更改选定的答案是一个不错的选择,但这很少发生。因此,SO不会给别人留下深刻的印象,除非SO表示有活动,否则人们通常不会重温他们问的老问题。作为侧边栏,我建议查看Fruity以获得基准,因为它不那么麻烦,并且更易于进行明智的测试。
Tin Man

930

您可以使用compact

[1, nil, 3, nil, nil].compact
=> [1, 3] 

我想提醒人们,如果您得到一个包含nils的数组作为一个map块的输出,并且该块试图有条件地返回值,那么您就有了代码味道,需要重新考虑一下逻辑。

例如,如果您正在执行以下操作:

[1,2,3].map{ |i|
  if i % 2 == 0
    i
  end
}
# => [nil, 2, nil]

那不要 相反,在之前mapreject您不需要或想要的东西select

[1,2,3].select{ |i| i % 2 == 0 }.map{ |i|
  i
}
# => [2]

我考虑compact用来清理混乱,这是最后的努力,以摆脱我们未能正确处理的事情,通常是因为我们不知道会发生什么。我们应该始终知道程序中会抛出什么样的数据。意外/未知数据是错误的。每当我看到正在处理的数组中的nils时,我都会研究它们为什么存在,并查看是否可以改进生成数组的代码,而不是让Ruby浪费时间和内存来生成nil,然后在数组中进行筛选以将其删除他们以后。

'Just my $%0.2f.' % [2.to_f/100]

29
现在就是红宝石风格!
Christophe Marois,

4
为什么要这样 OP需要剥离nil条目,而不是空字符串。顺便说一句,nil它不同于空字符串。
Tin Man

9
两种解决方案在整个集合中迭代两次……为什么不使用reduceinject
Ziggy 2015年

4
听起来好像您没有阅读OP的问题或答案。问题是,如何从数组中删除nil。compact最快,但是实际上在一开始就正确地编写了代码,因此无需完全处理nil。
Tin Man

3
我不同意!问题是“映射并删除零值”。好吧,映射和删除nil值是为了减少。在他们的示例中,OP会映射,然后选择nil。调用map然后压缩,或者先选择再映射,就等于犯了同样的错误:正如您在答案中指出的那样,这是一种代码味道。
Ziggy

96

尝试使用reduceinject

[1, 2, 3].reduce([]) { |memo, i|
  if i % 2 == 0
    memo << i
  end

  memo
}

我同意接受的答案,希望大家不要mapcompact,而不是出于同样的原因。

我觉得内心深处是map那么compact相当于select然后map。考虑:map是一对一功能。如果要从一组值映射为,则为map,则需要在输出集中为输入集中的每个值提供一个值。如果您需要select事先准备,那么您可能不希望map在此背景片上出现。如果您必须select事后(或compact),那么您可能不希望map在此背景上出现。无论哪种情况,您都需要遍历整个集合两次,而一次reduce只需要进行一次。

另外,您正在尝试用英语“将一组整数简化为一组偶数整数”。


4
Ziggy可怜,不喜欢您的建议。大声笑。加一,其他人有数百个赞!
DDDD 2015年

2
我相信,有一天,在您的帮助下,这个答案将超过公认的答案。^ o ^ //
Ziggy 2015年

2
+1当前接受的答案不允许您使用在选择阶段所执行操作的结果
凌晨

1
如果只需要通过,则对可枚举的数据结构进行两次迭代,就像在接受的答案中那样看起来很浪费。从而通过使用reduce减少通过次数!感谢@Ziggy
sebisnow

确实如此!但是对n个元素的集合进行两次遍历仍然是O(n)。除非您的集合太大而不能容纳到缓存中,否则进行两次遍历可能会很好(我只是认为这样做更优雅,更具表现力,并且在将来发生循环下降时,也不太可能在将来导致错误不同步)。如果您也喜欢一口气做事,您可能会对学习换能器感兴趣!github.com/cognitect-labs/transducers-ruby
Ziggy

33

在您的示例中:

items.map! { |x| process_x url } # [1, 2, 3, 4, 5] => [1, nil, 3, nil, nil]

除了被替换为以外,其他值似乎没有变化nil。如果是这样,则:

items.select{|x| process_x url}

就足够了。


27

如果您想要一个宽松的拒绝标准,例如,拒绝空字符串和nil,则可以使用:

[1, nil, 3, 0, ''].reject(&:blank?)
 => [1, 3, 0] 

如果您想走得更远并拒绝零值(或对流程应用更复杂的逻辑),则可以传递一个块以拒绝:

[1, nil, 3, 0, ''].reject do |value| value.blank? || value==0 end
 => [1, 3]

[1, nil, 3, 0, '', 1000].reject do |value| value.blank? || value==0 || value>10 end
 => [1, 3]

5
。空白?仅在导轨中可用。
ewalk

供以后参考,因为blank?仅在导轨中可用,我们可以使用items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]不与导轨耦合的。(尽管不会排除空字符串或0)
Fotis


4

each_with_object 可能是最干净的方法:

new_items = items.each_with_object([]) do |x, memo|
    ret = process_x(x)
    memo << ret unless ret.nil?
end

我认为,在有条件的情况下,each_with_object它优于inject/ reduce,因为您不必担心该块的返回值。


0

达到此目的的另一种方法如下所示。在这里,我们Enumerable#each_with_object用来收集值,并利用它Object#tap来消除nil检查process_x方法结果所需的临时变量。

items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}

完整的示例说明:

items = [1,2,3,4,5]
def process x
    rand(10) > 5 ? nil : x
end

items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}

替代方法:

通过查看您正在调用的方法process_x url,尚不清楚x该方法中输入的目的。如果我假设您要x通过传递一些值来处理的值,url并确定哪个xs真正被处理成有效的非nil结果-那么,可能Enumerabble.group_by是比更好的选择Enumerable#map

h = items.group_by {|x| (process x).nil? ? "Bad" : "Good"}
#=> {"Bad"=>[1, 2], "Good"=>[3, 4, 5]}

h["Good"]
#=> [3,4,5]
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.