407

我有一个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?

4

9 回答 9

1049

你可以使用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清理烂摊子作为最后的努力,以摆脱我们没有正确处理的事情,通常是因为我们不知道会发生什么。我们应该始终知道在我们的程序中抛出了什么样的数据;意外/未知数据是错误的。每当我在我正在处理的数组中看到 nil 时,我都会深入研究它们存在的原因,看看我是否可以改进生成数组的代码,而不是让 Ruby 浪费时间和内存来生成 nil,然后筛选数组以删除他们以后。

'Just my $%0.2f.' % [2.to_f/100]
于 2012-11-21T02:30:55.620 回答
102

尝试使用reduceinject

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

  memo
}

我同意我们不应该接受的答案mapcompact,但出于相同的原因。

我内心深处觉得mapthencompact就等于selectthen map。考虑:map是一个一对一的函数。如果您从一组值映射,并且您map,那么您希望输出集中的一个值对应输入集中的每个值。如果您必须事先准备select好,那么您可能不希望map在现场使用。如果您必须select事后(或compact),那么您可能不希望map在片场使用 a。在任何一种情况下,您都在整个集合上迭代两次,而 areduce只需要执行一次。

此外,在英语中,您正在尝试“将一组整数减少为一组偶数”。

于 2015-03-04T18:40:42.653 回答
66

红宝石 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 = 100_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)
于 2019-08-02T09:42:37.780 回答
39

绝对compact是解决此任务的最佳方法。但是,我们可以通过简单的减法获得相同的结果:

[1, nil, 3, nil, nil] - [nil]
 => [1, 3]
于 2013-11-21T17:35:20.257 回答
33

在您的示例中:

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

除了被替换之外,这些值看起来并没有改变nil。如果是这样的话,那么:

items.select{|x| process_x url}

就足够了。

于 2012-11-21T06:08:27.193 回答
30

如果你想要一个更宽松的拒绝标准,例如,拒绝空字符串和 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]
于 2014-08-11T21:22:01.670 回答
6

您可以#compact在结果数组上使用方法。

[10, nil, 30, 40, nil].compact => [10, 30, 40]
于 2021-07-05T13:28:49.887 回答
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_objectinject/更好reduce,因为您不必担心块的返回值。

于 2017-05-10T05:40:23.003 回答
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 真正被处理为有效的非零结果 - 那么,可能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]
于 2015-12-30T21:50:46.803 回答