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

Ruby风格:如何检查嵌套哈希元素是否存在,ruby,hash,coding-style,Ruby,Hash,Coding Style,考虑散列中存储的“person”。两个例子是: fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}} slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} 如果“person”没有任何子元素,“chil

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

fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} 
如果“person”没有任何子元素,“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方式”是什么?

可以使用默认值为{}-empty hash的hash。比如说,

dino = Hash.new({})
dino[:pet] = {:name => "Dino"}
dino_has_children = !dino[:person][:children].nil? #=> false
也适用于已创建的哈希:

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

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

has_children = slate[:person] && slate[:person][:children]
使用.nil?仅当您使用false作为占位符值时才需要,实际上这是很少见的。通常,您可以简单地测试它是否存在

更新:如果您使用的是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
此方法将检查过程中的每个步骤,并避免在调用nil时出错。对于浅层结构,实用性有些有限,但对于深嵌套结构,我发现它非常宝贵:

has_children = slate.dig(:person, :children)
您还可以使其更加健壮,例如,测试:children条目是否实际填充了:

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

你可以试着和我一起玩

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 {}
请注意,在rails中,您还可以执行以下操作:

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

您可以使用
AND和
gem:

require 'andand'

fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true
有关更多说明,请访问。

x = {:a => {:b => 'c'}}
y = {}
您可以这样检查x和y:

(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil
structure[:a] && structure[:a][:b]
另一种选择:

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

这里有一种方法,您可以对哈希和任何嵌套哈希中的任何falsy值进行深入检查,而无需对Ruby哈希类进行猴子补丁(请不要对Ruby类进行猴子补丁,这是您永远不应该做的事情)

(假设使用Rails,尽管您可以轻松地将其修改为在Rails之外工作)


使用Ruby 2.3,我们将支持安全导航操作员:

has_children
现在可以写成:

has_children = slate[:person]&.[](:children)
dig
也正在添加:

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

在此处简化上述答案:

创建一个递归散列方法,其值不能为零,如下所示

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

如果您不介意在键的值不存在时创建空哈希:)

传统上,您确实必须这样做:

(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil
structure[:a] && structure[:a][:b]
但是,Ruby 2.3添加了一个功能,使这种方式更加优雅:

structure.dig :a, :b # nil if it misses anywhere along the way
有一个名为
ruby\u dig
的gem将为您提供此补丁

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
这将把所有散列整平为一个,然后是任何一个?如果存在与子字符串“children”匹配的任何键,则返回true。
这可能也会有所帮助。

Thks@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

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

更新:不是覆盖括号访问器,而是请求哈希实例来扩展模块。 然后你可以做:

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"}}
我不知道“Ruby”是怎样的(!),但我写的gem基本上可以让你做到这一点,而不改变你原来的语法:

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

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

这使用了一些技巧来中介密钥访问调用。在
call
,它将尝试
dino
上的
dig
上的前几个键,如果遇到错误(它将返回nil)
nil?
然后当然返回true。

您可以使用
&
键的组合。
dig
相比,它是O(1),后者是O(n),这将确保访问人员时无需
NoMethodError:nil:NilClass的未定义方法“[]”

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

您没有为此实现对象模型或至少用验证方法修饰某些结构的任何原因。我想,如果你想在散列中添加语义,你会发疯的。即使你有一个对象模型,有时你也需要从散列中提取数据来填充你的模型。例如,如果您从JSON流获取数据,您将如何使用相同的方法在嵌套哈希中设置值?您必须编写一些创建中间哈希的内容,而不是简单地测试它们是否存在<如果您只处理哈希,那么代码>位置[键]| |={}
就足够了,但您必须提取最后一部分
final_key=path.pop
并最终分配给它。这是一个好主意吗?(哈希)-好主意。保持简洁明了:p@Josiah如果您在扩展核心类时遇到问题,最好完全避开Rails,这是一种常见现象。这可以使你的代码更干净。积极使用会导致混乱。@tadman通过这个SO答案供您参考,#dig现在在Ruby trunk::DYou必须确保。默认值嵌套在每个哈希中。如果您使用的是Ruby<2.3,我刚刚发布了一个gem,它添加了2.3兼容的哈希#dig和Array#dig方法:我考虑了@zeeraw的注释使用is#a?为了一个嵌套的散列,我想出了这个。
has_kids = !dino.dial[:person][:children].call.nil?
fred[:person]&.key?(:children) //=>true
slate[:person]&.key?(:children)