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

Posted

技术标签:

【中文标题】如何在 Ruby 中映射和删除 nil 值【英文标题】:How to map and remove nil values in Ruby 【发布时间】:2012-11-09 05:51:35 【问题描述】:

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

但这似乎不是惯用的。有没有一种很好的方法可以将函数映射到列表上,随时删除/排除 nil?

【问题讨论】:

Ruby 2.7 引入了filter_map,这似乎很适合这个。省去了重新处理数组的需要,而不是第一次就可以根据需要得到它。 More info here. 数组也有紧凑的 【参考方案1】:

你可以使用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]

【讨论】:

为什么要这样? OP 需要去除 nil 条目,而不是空字符串。顺便说一句,nil 与空字符串不同。 两种解决方案都在集合上迭代两次...为什么不使用reduceinject 这听起来不像您阅读了 OP 的问题或答案。问题是,如何从数组中删除 nil。 compact 是最快的,但实际上一开始就正确编写代码无需完全处理 nils。 我不同意!问题是“映射和删除零值”。好吧,映射和删除 nil 值就是减少。在他们的示例中,OP 映射然后选择 nil。调用 map 然后 compact 或 select 然后 map 就等于犯了同样的错误:正如您在回答中指出的那样,这是代码异味。 @Ziggy 的回答应该被接受为正确答案【参考方案2】:

尝试使用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 只需要遍历一次。

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

【讨论】:

可怜的 Ziggy,不喜欢你的建议。哈哈。加一,别人有数百个赞成票! 我相信有一天,在您的帮助下,这个答案会超过公认的。 ^o^// +1 当前接受的答案不允许您使用您在选择阶段执行的操作的结果 迭代可枚举的数据结构两次,如果只需要像在接受的答案中那样通过似乎很浪费。因此,通过使用 reduce! 来减少传递次数!谢谢@Ziggy 这是真的!但是对 n 个元素的集合进行两次传递仍然是 O(n)。除非您的集合太大以至于无法放入缓存中,否则执行两次可能就可以了(我只是认为这更优雅,更具表现力,并且将来在循环下降时不太可能导致错误不同步)。如果您也喜欢一次性完成,您可能有兴趣了解传感器! github.com/cognitect-labs/transducers-ruby【参考方案3】:

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 adds 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)

【讨论】:

不错!感谢您的更新 :) 一旦 Ruby 2.7.0 发布,我认为将接受的答案切换到这个可能是有意义的。不过,我不确定这里的礼仪是什么,您是否通常给现有接受的回复一个更新的机会?我认为这是引用 2.7 中新方法的第一个答案,因此应该成为公认的答案。 @the-tin-man 你同意这个观点吗? 感谢@PeterHamilton - 感谢您的反馈,并希望它对很多人有用。我很高兴支持你的决定,但显然我喜欢你提出的论点 :) 是的,这是有核心团队倾听的语言的好处。 建议更改选定的答案是一个很好的姿态,但这种情况很少发生。 SO 没有提供提醒人们的便利工具,人们通常不会重新审视他们提出的旧问题,除非 SO 表示有活动。作为侧边栏,我建议查看 Fruity 进行基准测试,因为它不那么繁琐,并且更容易进行明智的测试。【参考方案4】:

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

[1, nil, 3, nil, nil] - [nil]
 => [1, 3]

【讨论】:

是的,集合减法可以工作,但由于开销,它的速度大约是原来的一半。【参考方案5】:

在你的例子中:

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

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

items.select|x| process_x url

足够了。

【讨论】:

【参考方案6】:

如果您想要一个更宽松的拒绝标准,例如,拒绝空字符串和 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]

【讨论】:

.blank?仅在 Rails 中可用。 为了将来参考,由于blank? 仅在rails 中可用,我们可以使用items.reject!(&amp;:nil?) # [1, nil, 3, nil, nil] =&gt; [1, 3],它不与rails 耦合。 (但不会排除空字符串或 0)【参考方案7】:

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

[10, nil, 30, 40, nil].compact => [10, 30, 40]

【讨论】:

这个解决方案已经提供here【参考方案8】:

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 更好,因为您不必担心块的返回值。

【讨论】:

【参考方案9】:

实现它的另一种方法如下所示。在这里,我们使用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 的目的是什么。如果我假设您将通过传递一些url 来处理x 的值,并确定哪些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]

【讨论】:

以上是关于如何在 Ruby 中映射和删除 nil 值的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Ruby 中记忆一个可能返回 true、false 或 nil 的方法?

如何在 Ruby 中为 splat 参数设置默认值

如何使用XSLT映射删除xsi:type =“xsd:string”&xsi:nil =“true”?

如何使用pop函数删除ruby中数组的第零索引[重复]

如何在 Ruby 中编写复杂的多行 if 条件?

如何在 Ruby 的 MULTI 块中读取 Redis?