我有一个map
,它要么更改一个值,要么将其设置为nil。然后我想从列表中删除 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
但这似乎不是那么惯用。有没有一种很好的方法可以在列表上映射函数,随时删除/排除 nils?
您可以使用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]
那就不要了。相反,在map
之前,reject
你不想要的东西或select
你想要的东西:
[1,2,3].select{ |i| i % 2 == 0 }.map{ |i|
i
}
# => [2]
我认为用compact
来收拾烂摊子是摆脱我们没有正确处理的事情的最后努力,通常是因为我们不知道会发生什么。我们应该始终知道我们的程序中被抛出什么样的数据;意外/未知数据是错误的。每当我在我正在处理的数组中看到 nils 时,我都会深入研究它们存在的原因,看看我是否可以改进生成数组的代码,而不是让 Ruby 浪费时间和内存生成 nils,然后筛选数组以稍后删除它们。
'Just my $%0.2f.' % [2.to_f/100]
尝试使用reduce
或inject
。
[1, 2, 3].reduce([]) { |memo, i|
if i % 2 == 0
memo << i
end
memo
}
我同意公认的答案,即我们不应该map
和compact
,但不是出于同样的原因。
我内心深处觉得map
然后compact
相当于select
然后map
。考虑:map
是一对一的功能。如果要从一组值进行映射,并且map
,则需要在输出集中为输入集中的每个值提供一个值。如果你必须事先select
,那么你可能不想在片场map
。如果你不得不在之后(或compact
)select
,那么你可能不希望在片场出现map
。无论哪种情况,您都会在整个集合上迭代两次,而reduce
只需要迭代一次。
此外,在英语中,您正在尝试"将一组整数简化为一组偶数"。
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]
在您的情况下,当块的计算结果为 falsey 时,只需:
items.filter_map { |x| transform(x) }
"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)
绝对compact
是解决此任务的最佳方法。但是,我们只需一个简单的减法就可以达到相同的结果:
[1, nil, 3, nil, nil] - [nil]
=> [1, 3]
在您的示例中:
items.map! { |x| transform(x) } # [1, 2, 3, 4, 5] => [1, nil, 3, nil, nil]
除了替换为nil
之外,看起来值没有变化。如果是这种情况,那么:
items.select{|x| transform(x) }
就足够了。
如果你想要一个更宽松的拒绝标准,例如,拒绝空字符串和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]
您可以在生成的数组上使用#compact
方法。
[10, nil, 30, 40, nil].compact => [10, 30, 40]
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
更好,因为您不必担心块的返回值。
另一种实现方法如下所示。在这里,我们使用Enumerable#each_with_object
来收集值,并利用Object#tap
来摆脱检查transform x
方法结果nil
否则需要的临时变量。
items.each_with_object([]) {|x, obj| (transform x).tap {|r| obj << r unless r.nil?}}
完整的说明示例:
items = [1,2,3,4,5]
def transform x
rand(10) > 5 ? nil : x
end
items.each_with_object([]) {|x, obj| (transform x).tap {|r| obj << r unless r.nil?}}
>替代方法:通过查看您transform x
调用的方法,不清楚该方法中输入x
的目的是什么。 如果我假设您要通过传递一些url
来处理x
的值,并确定哪些x
真正被处理成有效的非 nil 结果 - 那么,可能是Enumerabble.group_by
是比Enumerable#map
更好的选择。
h = items.group_by {|x| (transform x).nil? ? "Bad" : "Good"}
#=> {"Bad"=>[1, 2], "Good"=>[3, 4, 5]}
h["Good"]
#=> [3,4,5]