Ruby风格:如何检查嵌套哈希元素是否存在

70

考虑一个存储在哈希表中的“人”对象。以下是两个例子:

fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} 
如果“人”没有子元素,“children”元素将不会出现。 因此,对于Slate先生,我们可以检查他是否有父母:
slate_has_children = !slate[:person][:children].nil?

那么,如果我们不知道“slate”是一个“person”哈希,会怎样呢?考虑以下情况:

dino = {:pet => {:name => "Dino"}}

我们现在无法轻易地检查子元素:

dino_has_children = !dino[:person][:children].nil?
NoMethodError: undefined method `[]' for nil:NilClass

那么,您如何检查哈希表的结构,特别是如果它嵌套得很深(甚至比这里提供的示例更深)?也许一个更好的问题是:用“Ruby方式”如何实现这个?


1
你为什么没有实现一个对象模型或者至少用一些验证方法装饰一些结构体呢?我认为你会因为试图在哈希表上添加语义而抓狂的。 - Chris McCauley
1
即使您拥有对象模型,有时也需要从哈希中提取数据以填充您的模型。例如,如果您从JSON流获取数据。 - paradigmatic
16个回答

0

感谢@tadman的回答。

对于那些想要性能(并且被困在ruby < 2.3中)的人,这种方法快了2.5倍:

unless Hash.method_defined? :dig
  class Hash
    def dig(*path)
      val, index, len = self, 0, path.length
      index += 1 while(index < len && val = val[path[index]])
      val
    end
  end
end

如果你使用RubyInline,这个方法会快16倍:

unless Hash.method_defined? :dig
  require 'inline'

  class Hash
    inline do |builder|
      builder.c_raw '
      VALUE dig(int argc, VALUE *argv, VALUE self) {
        rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
        self = rb_hash_aref(self, *argv);
        if (NIL_P(self) || !--argc) return self;
        ++argv;
        return dig(argc, argv, self);
      }'
    end
  end
end

0

你可以尝试玩一下

dino.default = {}

或者举个例子:

empty_hash = {}
empty_hash.default = empty_hash

dino.default = empty_hash

这样你就可以调用

empty_hash[:a][:b][:c][:d][:e] # and so on...
dino[:person][:children] # at worst it returns {}

0

你也可以定义一个模块来为方括号方法设置别名,并使用 Ruby 语法读取/写入嵌套元素。

更新:不要覆盖方括号访问器,而是请求 Hash 实例扩展该模块。

module Nesty
  def []=(*keys,value)
    key = keys.pop
    if keys.empty? 
      super(key, value) 
    else
      if self[*keys].is_a? Hash
        self[*keys][key] = value
      else
        self[*keys] = { key => value}
      end
    end
  end

  def [](*keys)
    self.dig(*keys)
  end
end

class Hash
  def nesty
    self.extend Nesty
    self
  end
end

然后你可以这样做:

irb> a = {}.nesty
=> {}
irb> a[:a, :b, :c] = "value"
=> "value"
irb> a
=> {:a=>{:b=>{:c=>"value"}}}
irb> a[:a,:b,:c]
=> "value"
irb> a[:a,:b]
=> {:c=>"value"}
irb> a[:a,:d] = "another value"
=> "another value"
irb> a
=> {:a=>{:b=>{:c=>"value"}, :d=>"another value"}}

0

给定

x = {:a => {:b => 'c'}}
y = {}

你可以这样检查xy

(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil

0

我不知道这有多"Ruby",但我写的KeyDial gem让你基本上可以在不改变原始语法的情况下做到这一点:

has_kids = !dino[:person][:children].nil?

变成:

has_kids = !dino.dial[:person][:children].call.nil?

这里使用了一些诡计来中介访问键的调用。在call中,它会尝试在dino上dig前面的键,如果遇到错误(它肯定会遇到),则返回nil。nil?当然就返回true了。


0

您可以使用&key?的组合,它是O(1)的,相比之下dig是O(n),这将确保访问人员时不会出现NoMethodError: undefined method `[]' for nil:NilClass的错误。

fred[:person]&.key?(:children) //=>true
slate[:person]&.key?(:children)

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