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

考虑存储在散列中的“人”。 两个例子是:

fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}} slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} 

如果“人”没有孩子,则“孩子”元素不存在。 那么,对于斯莱特先生,我们可以检查他是否有父母:

 slate_has_children = !slate[:person][:children].nil? 

那么,如果我们不知道“石板”是一个“人”哈希呢? 考虑:

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

我们再也不能轻易地检查孩子了:

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

那么,你将如何检查一个散列的结构,特别是如果它深深地嵌套(甚至比这里提供的例子更深)呢? 也许更好的问题是:做这个“Ruby方式”是什么?

最明显的方法是简单地检查每个步骤:

 has_children = slate[:person] && slate[:person][:children] 

使用.nil? 只有当你使用false作为占位符值时才是必需的,实际上这很less见。 一般来说你可以简单地testing它的存在

更新 :如果您使用Ruby 2.3或更高版本,则有一个内置的dig方法,可以完成此答案中所述的内容。

如果没有,你也可以定义你自己的哈希“挖”的方法,可以大大简化这个:

 class Hash def dig(*path) path.inject(self) do |location, key| location.respond_to?(:keys) ? location[key] : nil end end end 

这种方法将检查每一步的方法,避免绊倒调用为零。 对于浅层结构,效用是有限的,但是对于深层嵌套结构,我觉得这是非常宝贵的:

 has_children = slate.dig(:person, :children) 

你也可以使这个更健壮,例如,testing:children条目是否实际上被填充:

 children = slate.dig(:person, :children) has_children = children && !children.empty? 

在Ruby 2.3中,我们将支持安全的导航操作: https : //www.ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released/

has_children现在可以写成:

 has_children = slate[:person]&.[](:children) 

dig也被添加:

 has_children = slate.dig(:person, :children) 

另一种select:

 dino.fetch(:person, {})[:children] 

你可以使用和gem:

 require 'andand' fred[:person].andand[:children].nil? #=> false dino[:person].andand[:children].nil? #=> true 

你可以在http://andand.rubyforge.org/find更多的解释。;

人们可以使用哈希与默认值{} – 空散列。 例如,

 dino = Hash.new({}) dino[:pet] = {:name => "Dino"} dino_has_children = !dino[:person][:children].nil? #=> false 

这与已经创build的哈希一起工作:

 dino = {:pet=>{:name=>"Dino"}} dino.default = {} dino_has_children = !dino[:person][:children].nil? #=> false 

或者你可以定义nil类的[]方法

 class NilClass def [](* args) nil end end nil[:a] #=> nil 

传统上,你真的不得不这样做:

 structure[:a] && structure[:a][:b] 

但是,Ruby 2.3增加了一个使得这种方式更加优雅的特性:

 structure.dig :a, :b # nil if it misses anywhere along the way 

有一个名为ruby_dig的gem会为你补上这个。

 dino_has_children = !dino.fetch(person, {})[:children].nil? 

请注意,在rails中,你也可以这样做:

 dino_has_children = !dino[person].try(:[], :children).nil? # 

这里有一种方法可以深入检查散列中的任何falsy值,以及没有猴子修补Ruby Hash类的任何嵌套散列(请不要在Ruby类中修改修补程序,这是你不应该做的,EVER) 。

(假设Rails,虽然你可以很容易地修改这个在Rails之外工作)

 def deep_all_present?(hash) fail ArgumentError, 'deep_all_present? only accepts Hashes' unless hash.is_a? Hash hash.each do |key, value| return false if key.blank? || value.blank? return deep_all_present?(value) if value.is_a? Hash end true end 
 def flatten_hash(hash) hash.each_with_object({}) do |(k, v), h| if v.is_a? Hash flatten_hash(v).map do |h_k, h_v| h["#{k}_#{h_k}"] = h_v end else h[k] = v end end end irb(main):012:0> fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}} => {:person=>{:name=>"Fred", :spouse=>"Wilma", :children=>{:child=>{:name=>"Pebbles"}}}} irb(main):013:0> slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} => {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}} irb(main):014:0> flatten_hash(fred).keys.any? { |k| k.include?("children") } => true irb(main):015:0> flatten_hash(slate).keys.any? { |k| k.include?("children") } => false 

这将所有的哈希变成一个,然后呢? 如果存在任何与子string“children”匹配的键,则返回true。 这也可能有帮助。

你可以尝试玩

 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 {} 

特定

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

你可以像这样检查xy

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

简化上面的答案:

创build一个recursionHash方法,其值不能为零,如下所示。

 def recursive_hash Hash.new {|key, value| key[value] = recursive_hash} end > slate = recursive_hash > slate[:person][:name] = "Mr. Slate" > slate[:person][:spouse] = "Mrs. Slate" > slate => {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}} slate[:person][:state][:city] => {} 

如果你不介意创build空的哈希如果不存在:)

Thks @tadman的答案。

对于那些想要perfs(并与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 

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

更新:而不是覆盖括号访问器,请求哈希实例扩展模块。

 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"}}