如何避免在嵌套哈希中缺少元素时出现NoMethodError,而无需重复使用nil检查?

32

我正在寻找一种避免在深度嵌套的哈希表中每个级别检查nil的好方法。例如:

name = params[:company][:owner][:name] if params[:company] && params[:company][:owner] && params[:company][:owner][:name]

这需要进行三个检查,会导致代码非常丑陋。有没有什么方法可以避免这种情况?


1
在Groovy中,您将使用?运算符。实际上,我对等效运算符很感兴趣。您仍然可以扩展哈希类并添加该运算符。 - Pasta
@Pasta Io 有一个类似的运算符,但 Ruby 没有。 - Phrogz
16个回答

37

Ruby 2.3.0版本在HashArray上引入了一种名为dig的方法

name = params.dig(:company, :owner, :name)

如果在任何层级上键值缺失,它将返回nil

如果你使用的是2.3版本以前的Ruby,你可以安装像ruby_dighash_dig_and_collect这样的gem,或者自己实现这个功能:

module RubyDig
  def dig(key, *rest)
    if value = (self[key] rescue nil)
      if rest.empty?
        value
      elsif value.respond_to?(:dig)
        value.dig(*rest)
      end
    end
  end
end

if RUBY_VERSION < '2.3'
  Array.send(:include, RubyDig)
  Hash.send(:include, RubyDig)
end

如果 paramsnil,那么 params.dig 将会失败。考虑使用安全导航操作符或与 .dig 结合使用,例如:params&.dig(:company, :owner, :name)params&.company&.owner&.name - thisismydesign
2
我之前评论中关于哈希表安全导航操作符的语法是错误的。正确的语法应该是:params&.[](:company)&.[](:owner)&.[](:name) - thisismydesign

11

在我看来,功能和清晰度之间的最佳折中方案是Raganwald的andand。使用它,您可以这样做:

params[:company].andand[:owner].andand[:name]

这与try相似,但在本例中读起来更好,因为您仍然像平常一样发送消息,但在调用引起注意的分隔符之间处理nils。


1
+1:我本来想推荐 Raganwald 的 Ick,它和 maybe 是一样的思路。你也可以在回答中包含一个链接:http://ick.rubyforge.org/。 - tokland
3
IMO andand 在语法上很丑陋。 - mpd
@mpd:为什么?是因为概念上的问题还是你不喜欢那个特定的词? - Chuck
@chuck,我喜欢这个概念,但它似乎不够优雅。如果你不知道它被用来做什么,它也会让人感到困惑,我的意思是andand根本没有意义(我理解它与&&的引用)。我认为它的名称并没有恰当地传达它的含义。话虽如此,我还是比try更喜欢它。 - mpd

7

我不知道那是否是您所需要的,但也许您可以这样做?

name = params[:company][:owner][:name] rescue nil

11
很抱歉说这句话,但是不加区分的救援是有害的,因为它可能掩盖了许多与问题无关的错误... - tokland
是的,用大写字母"E"来形容邪恶。 - the Tin Man
4
由于这里发生的唯一事情是符号哈希查找,因此对我来说,这似乎是一种非常有针对性的救援行动,也正是我所做的。 - glenn mcdonald
您可以选择要捕获的异常,像这样:https://dev59.com/uG025IYBdhLWcg3wFxua#17686902 - Nicolas Goy
@glennmcdonald 这段代码并不能确保params是一个哈希表。使用rescue nil仍然不可取。这里已经发布了更好、更简单的解决方案。没有理由去冒险尝试聪明地处理这个问题。 - thisismydesign

4

更新:此答案已经过时。请使用当前被接受的答案所建议的dig

如果是Rails,请使用

params.try(:[], :company).try(:[], :owner).try(:[], :name)

哦,等等,那甚至更丑陋。;-)


我不会说它更丑陋。谢谢你的回复,Kyle。 - Kevin Sylvestre

4

这段文字的意思是:与用户mpd建议的第二个解决方案相同,只不过更符合Ruby语言的惯用方式。

class Hash
  def deep_fetch *path
    path.inject(self){|acc, e| acc[e] if acc}
  end
end

hash = {a: {b: {c: 3, d: 4}}}

p hash.deep_fetch :a, :b, :c
#=> 3
p hash.deep_fetch :a, :b
#=> {:c=>3, :d=>4}
p hash.deep_fetch :a, :b, :e
#=> nil
p hash.deep_fetch :a, :b, :e, :f
#=> nil

1
这里有一个稍微改进的方法:https://dev59.com/uG025IYBdhLWcg3wFxua - Josh
这里有一个比“稍微改进”的方法略微更好一些:https://dev59.com/uG025IYBdhLWcg3wFxua#27498050 - Benjamin Dobell

4

谢谢Stephen。我以前从未听说过自动实例化,但如果我正在定义哈希表,它将非常完美。感谢您的回答! - Kevin Sylvestre
你可以编辑你的回答,让链接更加明显。很难分辨最后两个链接指向哪里。 - the Tin Man

3
如果你想进入monkeypatching,可以像这样做:
```ruby class NilClass def [](anything) nil end end ```
然后,当嵌套的哈希中任何一个为空时,对`params[:company][:owner][:name]`的调用将产生nil。
编辑: 如果你想要更安全的路线,并且提供干净的代码,可以这样做
```ruby class Hash def chain(*args) x = 0 current = self[args[x]] while current && x < args.size - 1 x += 1 current = current[args[x]] end current end end ```
代码看起来像这样:`params.chain(:company, :owner, :name)`。

2
我喜欢这个解决方案,因为它很聪明,可以带来非常干净的代码。但是,对我来说,它确实感觉很危险。你永远不知道整个应用程序中是否有数组实际上是nil。 - Matt Greer
1
是的,这种方法的一个很大的缺点。然而,在方法定义中可以进行一些其他的技巧来警告您发生这种情况时。这只是一个想法,可以根据程序员的需求进行调整。 - mpd
3
这段代码可以运行,但有一定危险性,因为你在对 Ruby 的一个非常基础的部分进行所谓的“猴子补丁”,使其以一种完全不同的方式工作。 - Chuck
是的,我仍然非常害怕猴子补丁! - Kevin Sylvestre

2
我会将这个写成:

我会将此写为:

name = params[:company] && params[:company][:owner] && params[:company][:owner][:name]

虽然 Ruby 没有像 Io 中的 ? 运算符 那样简洁,但它仍然可以完成相同的功能。@ThiagoSilveira 给出的答案也不错,尽管可能会慢一些。


1

做:

params.fetch('company', {}).fetch('owner', {})['name']

在每个步骤中,你都可以使用NilClass中适当的方法来避免nil,如果它是数组、字符串或数字的话。只需将to_hash添加到此列表的清单中并使用即可。
class NilClass; def to_hash; {} end end
params['company'].to_hash['owner'].to_hash['name']

1

你能避免使用多维哈希表,而使用

params[[:company, :owner, :name]]

或者

params[[:company, :owner, :name]] if params.has_key?([:company, :owner, :name])

instead?


感谢您的回复,安德鲁。不幸的是,我无法避免多维哈希,因为哈希是从外部库传递的。 - Kevin Sylvestre

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接