Ruby Style:如何检查嵌套的哈希元素是不是存在
Posted
技术标签:
【中文标题】Ruby Style:如何检查嵌套的哈希元素是不是存在【英文标题】:Ruby Style: How to check whether a nested hash element existsRuby Style:如何检查嵌套的哈希元素是否存在 【发布时间】:2010-12-21 16:25:08 【问题描述】:考虑一个存储在哈希中的“人”。两个例子是:
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 方式”是什么?
【问题讨论】:
您没有为此实现对象模型或至少使用验证方法修饰某些结构的任何原因。我认为您会在尝试将语义添加到哈希中时让自己发疯。 即使您有一个对象模型,有时您也需要从哈希中提取数据来填充您的模型。例如,如果您从 JSON 流中获取数据。 【参考方案1】:最明显的方法是简单地检查每一步:
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?
【讨论】:
您将如何使用相同的方法在嵌套哈希中设置值? 您必须编写一些东西来创建中间散列,而不是简单地测试它们是否存在。如果您只处理哈希,location[key] ||=
就足够了,但您必须提取最后一部分 final_key = path.pop
并在最后分配给它。
is_a?(hash) - 好主意。保持简洁明了:p
@Josiah 如果您在扩展核心类时遇到问题,最好完全避开 Rails,它在那里很流行。谨慎使用它可以使您的代码更清洁。积极使用会导致混乱。
@tadman fyi 通过这个 SO 答案,#dig 现在在 Ruby 主干中:ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released:D【参考方案2】:
在 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)
【讨论】:
如果您使用的是 Ruby rubygems.org/gems/ruby_dig【参考方案3】:另一种选择:
dino.fetch(:person, )[:children]
【讨论】:
【参考方案4】:您可以使用andand
gem:
require 'andand'
fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true
您可以在http://andand.rubyforge.org/找到更多解释。
【讨论】:
【参考方案5】:可以使用默认值为 的散列 - 空散列。例如,
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
【讨论】:
您必须确保 .default 嵌套在每个散列的深处。【参考方案6】:传统上,您确实必须这样做:
structure[:a] && structure[:a][:b]
但是,Ruby 2.3 添加了一个特性,使这种方式更加优雅:
structure.dig :a, :b # nil if it misses anywhere along the way
有一个名为 ruby_dig
的 gem 会为你回补这个。
【讨论】:
【参考方案7】: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。 这可能也有帮助。
【讨论】:
我对@zeeraw 的评论使用 is_a?用于嵌套哈希并想出了这个。【参考方案8】:dino_has_children = !dino.fetch(person, )[:children].nil?
请注意,在 Rails 中您也可以这样做:
dino_has_children = !dino[person].try(:[], :children).nil? #
【讨论】:
【参考方案9】:这是一种无需对 Ruby 哈希类进行猴子修补即可对哈希中的任何虚假值和任何嵌套哈希进行深度检查的方法(请不要对 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
【讨论】:
【参考方案10】:在此处简化上述答案:
创建一个Recursive Hash方法,其值不能为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]
=>
如果您不介意在键的值不存在的情况下创建空哈希:)
【讨论】:
【参考方案11】:你可以试试玩
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
【讨论】:
【参考方案12】:给定
x = :a => :b => 'c'
y =
你可以像这样检查 x 和 y:
(x[:a] || )[:b] # 'c'
(y[:a] || )[:b] # nil
【讨论】:
【参考方案13】:感谢@tadman 的回答。
对于那些想要 perfs(并且被 ruby
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
【讨论】:
【参考方案14】:您还可以定义一个模块来为方括号方法设置别名,并使用 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"
【讨论】:
【参考方案15】:我不知道它有多“Ruby”(!),但我编写的 KeyDial gem 让您可以在不更改原始语法的情况下完成此操作:
has_kids = !dino[:person][:children].nil?
变成:
has_kids = !dino.dial[:person][:children].call.nil?
这使用了一些技巧来中间键访问调用。在call
,它将尝试dig
dino
上的前一个键,如果它遇到错误(因为它会),则返回 nil。 nil?
然后当然返回 true。
【讨论】:
【参考方案16】:您可以使用 &
和 key?
的组合,它是 O(1),而 dig
是 O(n),这将确保在没有 NoMethodError: undefined method `[]' for nil:NilClass
的情况下访问人员
fred[:person]&.key?(:children) //=>true
slate[:person]&.key?(:children)
【讨论】:
以上是关于Ruby Style:如何检查嵌套的哈希元素是不是存在的主要内容,如果未能解决你的问题,请参考以下文章