提问人:Todd R 提问时间:11/30/2009 最后编辑:Gaurav SharmaTodd R 更新时间:5/23/2019 访问量:60877
Ruby Style:如何检查嵌套的哈希元素是否存在
Ruby Style: How to check whether a nested hash element exists
问:
考虑存储在哈希中的“人”。两个例子是:
fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}}
如果“person”没有任何子元素,则“children”元素不存在。所以,对于斯莱特先生,我们可以检查他是否有父母:
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 方式”是什么?
答:
可以使用默认值为 {} 的哈希 - 空哈希。例如
dino = Hash.new({})
dino[:pet] = {:name => "Dino"}
dino_has_children = !dino[:person][:children].nil? #=> false
这也适用于已经创建的 Hash:
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
方法可以执行本答案中描述的操作。
如果没有,您还可以定义自己的 Hash “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?
评论
location[key] ||= { }
final_key = path.pop
你可以尝试玩
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 {}
dino_has_children = !dino.fetch(person, {})[:children].nil?
请注意,在 rails 中,您还可以执行以下操作:
dino_has_children = !dino[person].try(:[], :children).nil? #
您可以使用 Gem:andand
require 'andand'
fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true
您可以在 http://andand.rubyforge.org/ 找到进一步的解释。
鉴于
x = {:a => {:b => 'c'}}
y = {}
您可以像这样检查 X 和 Y:
(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil
另一种选择:
dino.fetch(:person, {})[:children]
这里有一种方法,你可以对哈希中的任何虚假值和任何嵌套的哈希进行深入检查,而无需对 Ruby Hash 类进行猴子修补(请不要在 Ruby 类上修补猴子,这是你不应该做的事情,永远)。
(假设是 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
在 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)
评论
在这里简化上述答案:
创建一个值不能为 nil 的递归哈希方法,如下所示。
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]
=> {}
如果您不介意创建空哈希,如果键的值不存在,则:)
传统上,你真的必须做这样的事情:
structure[:a] && structure[:a][:b]
然而,Ruby 2.3 增加了一个特性,使这种方式更加优雅:
structure.dig :a, :b # nil if it misses anywhere along the way
有一个叫做宝石的宝石会为你回修这个。ruby_dig
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
如果你使用 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 语法来读/写嵌套元素。
更新:不要重写括号访问器,而是请求 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"}}
我不知道它有多“Ruby”(!),但是我编写的KeyDial gem基本上可以让你在不改变原始语法的情况下做到这一点:
has_kids = !dino[:person][:children].nil?
成为:
has_kids = !dino.dial[:person][:children].call.nil?
这使用一些技巧来中介密钥访问调用。在 ,它将尝试 上一个键,如果它遇到错误(因为它会),则返回 nil。 然后当然返回 true。call
dig
dino
nil?
您可以使用 和 O(1) 的组合,而 O(n) 是 O(n),这将确保在没有&
key?
dig
NoMethodError: undefined method `[]' for nil:NilClass
fred[:person]&.key?(:children) //=>true
slate[:person]&.key?(:children)
评论