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 = 

你可以像这样检查 xy

(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,它将尝试digdino 上的前一个键,如果它遇到错误(因为它会),则返回 nil。 nil? 然后当然返回 true。

【讨论】:

【参考方案16】:

您可以使用 &amp;key? 的组合,它是 O(1),而 dig 是 O(n),这将确保在没有 NoMethodError: undefined method `[]' for nil:NilClass 的情况下访问人员

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

【讨论】:

以上是关于Ruby Style:如何检查嵌套的哈希元素是不是存在的主要内容,如果未能解决你的问题,请参考以下文章

Ruby 修改嵌套哈希的元素

如何递归检查ruby hash成员是不是存在?

Ruby如何格式化嵌套哈希

根据嵌套哈希的内容从哈希中排除元素?

使用键数组遍历嵌套的 Ruby 哈希

ruby 将嵌套数组转换为哈希