我有一个地图,改变一个值或设置它为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的数组作为映射块的输出,并且该块试图有条件地返回值,那么您就有了代码气味,需要重新考虑您的逻辑。
例如,如果你正在做这样的事情:
[1,2,3].map{ |i|
if i % 2 == 0
i
end
}
# => [nil, 2, nil]
那就不要。相反,在地图之前,拒绝你不想要的东西或选择你想要的东西:
[1,2,3].select{ |i| i % 2 == 0 }.map{ |i|
i
}
# => [2]
I consider using compact to clean up a mess as a last-ditch effort to get rid of things we didn't handle correctly, usually because we didn't know what was coming at us. We should always know what sort of data is being thrown around in our program; Unexpected/unknown data is bad. Anytime I see nils in an array I'm working on, I dig into why they exist, and see if I can improve the code generating the array, rather than allow Ruby to waste time and memory generating nils then sifting through the array to remove them later.
'Just my $%0.2f.' % [2.to_f/100]
如果你想要一个更宽松的拒绝标准,例如,拒绝空字符串以及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]
尝试使用reduce或inject。
[1, 2, 3].reduce([]) { |memo, i|
if i % 2 == 0
memo << i
end
memo
}
我同意公认的答案,我们不应该映射和压缩,但不是出于同样的原因。
I feel deep inside that map then compact is equivalent to select then map. Consider: map is a one-to-one function. If you are mapping from some set of values, and you map, then you want one value in the output set for each value in the input set. If you are having to select before-hand, then you probably don't want a map on the set. If you are having to select afterwards (or compact) then you probably don't want a map on the set. In either case you are iterating twice over the entire set, when a reduce only needs to go once.
此外,在英语中,您正在尝试“将一组整数缩减为一组偶数”。
完成它的另一种方法如下所示。在这里,我们使用Enumerable#each_with_object来收集值,并使用object# tap来消除临时变量,否则需要对process_x方法的结果进行nil检查。
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的目的是什么。如果我假设你将通过传递一些url来处理x的值,并确定哪些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]
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添加了可枚举的#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)