【问题标题】:(Not #dig!) How to determine a key *exists* in a deeply nested Ruby Hash?(不是#dig!)如何确定深度嵌套的Ruby Hash中的键*存在*?
【发布时间】:2021-03-21 21:14:43
【问题描述】:

有没有一种“简单”的方法,除了手写Hash#dig 执行的那种嵌套哈希/数组遍历,我可以确定一个键是否存在于深度嵌套的哈希中?另一种询问方式是说“确定是否分配了任何值”。

没有分配任何内容的 Hash 与明确分配了 nil 之间存在差异 - 特别是如果 Hash 是使用与 nil 不同的缺失键默认值构造的!

h = { :one => { :two => nil }}
h.dig(:one, :two).nil? # => true; but :two *is* present; it is assigned "nil". 
h[:one].key?(:two) # => true, because the key exists

h = { :one => {}}
h.dig(:one, :two).nil? # => true; :two *is not* present; no value is assigned.
h[:one].key?(:two) # => FALSE, because the key does not exist

【问题讨论】:

  • stackoverflow.com/questions/1820451/… 可能是一种方式。 stackoverflow.com/questions/15031412/… 是另一个。简短的回答是否定的,没有。你的用例是什么?
  • 第一个最终建议使用#dig 和另外两个手动编码Ruby 迭代器(在这些情况下,它不处理数组,因此无论如何在功能上不等同于#dig)。用例是一个复杂的嵌套哈希/数组入站有效负载,通过模式对象递归迭代,提供到线性属性集的所有可能映射,路径数组保持位置。我可以手写一种“#dig?”,它会很优雅,但我很可能只是重新设计迭代器方法,使其不那么优雅,但维护成本也更低,而不是这样做。
  • 更具体一点:这是一个 SCIM v2 实现,它利用了 ScimEngine、ScimRails 和 SCIM Query Filter Parser 中先前但所有情况下不完整的工作,以提供更全面的解决方案。手头的问题来自tools.ietf.org/html/rfc7644#section-3.5.1 描述的 PUT 语义,我希望在其中保持“可能假定客户端不断言”行为。一旦功能完成并经过测试,我们将在 MIT 许可下发布这项工作。
  • 让我们让您的问题更准确。如果h = { :one => { :two => { :four => nil }, :three => { :five => nil } } } 你可能会问是否有一个嵌套散列,比如:four 是一个键。您可以使用递归来确认是否存在这样的密钥,并且如果需要,可以生成一个向下钻取的密钥序列。但这不是你要问的。您的问题可能是“h 是否有一个键 :one,其值是具有键 :two 的散列,其值是具有键 :four 的散列?”。您可以轻松地将其转换为代码,使用dig 或不使用...
  • ...使用digg = h.dig(:one, :two); g.is_a?(Hash) && g.key?(:four)

标签: ruby ruby-hash


【解决方案1】:

如果您纯粹是检查密钥是否存在,您可以将digkey? 结合使用。在您的一系列按键中的最后一个或最后一个按键上使用 key?

input_hash = {
  hello: {
    world: {
      existing: nil,
    }
  }
}

# Used !! to make the result boolean

!!input_hash.dig(:hello, :world)&.key?(:existing) # => true
!!input_hash.dig(:hello, :world)&.key?(:not_existing) # => false
!!input_hash.dig(:hello, :universe)&.has_key?(:not_existing) # => false

【讨论】:

  • 是的,在核心库中没有任何“更好”的东西的情况下,这是一个不错的方法。如果终止节点是数组索引(在我的特定用例下不是问题),它可能会遇到的唯一问题是检查值是否存在。
【解决方案2】:

受您的核心扩展建议的启发,我稍微更新了实现以更好地模仿#dig

  • 需要 1+ 个参数
  • 如果挖掘没有返回nil,则引发TypeError,生成的对象不会响应dig?,并且还有其他参数要“挖掘”
module Diggable
  def dig?(arg,*args)
    return self.member?(arg) if args.empty?
    if val = self[arg] and val.respond_to?(:dig?) 
      val.dig?(*args)
    else
     val.nil? ? false : raise(TypeError, "#{val.class} does not have a #dig? method")
    end
  end
end

[Hash,Struct,Array].each { |klass| klass.send(:include,Diggable) }

class Array
  def dig?(arg,*args)
    return arg.abs < self.size if args.empty?
    super
  end
end

if defined?(OpenStruct)
  class OpenStruct
    def dig?(arg,*args)
      self.to_h.dig?(arg,*args)
    end
  end
end

用法

Foo = Struct.new(:a)

hash = {:one=>1, :two=>[1, 2, 3], :three=>[{:one=>1, :two=>2}, "hello", Foo.new([1,2,3]), {:one=>{:two=>{:three=>3}}}]}

hash.dig? #=> ArgumentError
hash.dig?(:one) #=> true
hash.dig?(:two, 0) #=> true
hash.dig?(:none) #=> false
hash.dig?(:none, 0) #=> false
hash.dig?(:two, -1) #=> true
hash.dig?(:two, 10) #=> false
hash.dig?(:three, 0, :two) #=> true
hash.dig?(:three, 0, :none) #=> false
hash.dig?(:three, 2, :a) #=> true
hash.dig?(:three, 3, :one, :two, :three, :f) #=> TypeError

Example

【讨论】:

  • 做得很好,内容丰富且具有教育意义。我唯一的问题是包含可选的self.,但我承认这是一个风格问题。
  • @CarySwoveland 一些self 引用是必需的,例如self[arg]。一般来说,我使用self,就像我在这里一样,用于扩展功能,否则被调用的方法的上下文对读者来说是“未知的”,并且可能被误认为是未定义的局部变量。
  • 这是我的假设。
【解决方案3】:

作为参考 - 采取不寻常的步骤来回答我自己的问题 ;-) - 如果我只是想写很多 Ruby,这是我可以解决这个问题的几种方法之一。

def dig?(obj, *args)
  arg = args.shift()

  return case obj
    when Array
      if args.empty?
        arg >= 0 && arg <= obj.size
      else
        dig?(obj[arg], *args)
      end
    when Hash
      if args.empty?
        obj.key?(arg)
      else
        dig?(obj[arg], *args)
      end
    when nil
      false
    else
      raise ArgumentError
  end
end

当然,如果您更喜欢核心扩展而不是显式方法,也可以打开 Array 和 Hash 之类的类并将#dig? 添加到这些类中:

class Hash
  def dig?(*args)
    arg = args.shift()

    if args.empty?
      self.key?(arg)
    else
      self[arg]&.dig?(*args) || false
    end
  end
end

class Array
  def dig?(*args)
    arg = args.shift()

    if args.empty?
      arg >= 0 && arg <= self.size
    else
      self[arg]&.dig?(*args) || false
    end
  end
end

...如果#dig? 参数导致非哈希/数组节点,则将引发NoMethodError 而不是ArgumentError

显然,可以将它们压缩成更巧妙/优雅的解决方案,使用更少的行,但以上的好处是恕我直言,非常容易阅读。

不过,在原始问题的范围内,希望更多地依赖于 Ruby 开箱即用的任何东西。我们早就集体承认没有单一方法的解决方案,但the answer from @AmazingRein 通过重用#dig 来避免递归。我们可以修改如下:

def dig?(obj, *args)
  last_arg = args.pop()
  obj      = obj.dig(*args) unless args.empty?

  return case obj
    when Array
      last_arg >= 0 && last_arg <= obj.size
    when Hash
      obj.key?(last_arg)
    when nil
      false
    else
      raise ArgumentError
  end
end

...考虑到所有因素,这还不错。

# Example test...

hash = {:one=>1, :two=>[1, 2, 3], :three=>[{:one=>1, :two=>2}, "hello", {:one=>{:two=>{:three=>3}}}]}

puts dig?(hash, :one)
puts dig?(hash, :two, 0)
puts dig?(hash, :none)
puts dig?(hash, :none, 0)
puts dig?(hash, :two, -1)
puts dig?(hash, :two, 10)
puts dig?(hash, :three, 0, :two)
puts dig?(hash, :three, 0, :none)
puts dig?(hash, :three, 2, :one, :two, :three)
puts dig?(hash, :three, 2, :one, :two, :none)

【讨论】:

  • 为自己的问题写一个答案并不奇怪,我理解你的意思。也就是说,恕我直言,Array#dig 有一个缺陷,即在使用“非隐式可转换为整数”索引时引发错误。这与其在其他情况下的行为不一致。例如,为什么[].dig(:key)) 会引发错误而[].dig(0,:key) 不会?
  • 是的,根据要求和您希望它的容错程度,您可能不需要#dig。就我而言,我认为我很高兴,因为尝试在具有非数字值的数组上调用 [] 通常会返回错误。我希望给定的路径在结构上是有效的,但我只是不知道该结构的所有部分是否都存在。
  • 根据您修改核心类的概念发布了答案。就参数和错误而言,它的行为方式应该与dig 相同,但它返回一个布尔值。感谢这个有趣的项目。
【解决方案4】:

这是一种简洁的方法,适用于嵌套的 ArrayHash(以及任何其他响应 fetch 的对象)。

def deep_fetch? obj, *argv
  argv.each do |arg|
    return false unless obj.respond_to? :fetch
    obj = obj.fetch(arg) { return false }
  end
  true
end

obj = { hello: [ nil, { world: nil } ] }
deep_fetch? obj, :hell # => false
deep_fetch? obj, :hello, 0 # => true
deep_fetch? obj, :hello, 2 # => false
deep_fetch? obj, :hello, 0, :world # => false
deep_fetch? obj, :hello, 1, :world # => true
deep_fetch? obj, :hello, :world
TypeError (no implicit conversion of Symbol into Integer)

之前的代码在访问具有非整数索引的数组元素时引发错误(就像Array#dig),这有时不是人们正在寻找的行为。以下代码在所有情况下都运行良好,但 rescue 不是一个好习惯:

def deep_fetch? obj, *argv
  argv.each { |arg| obj = obj.fetch(arg) } and true rescue false
end

obj = { hello: [ nil, { world: nil } ] }
deep_fetch? obj, :hell # => false
deep_fetch? obj, :hello, 0 # => true
deep_fetch? obj, :hello, 2 # => false
deep_fetch? obj, :hello, 0, :world # => false
deep_fetch? obj, :hello, 1, :world # => true
deep_fetch? obj, :hello, :world # => false

【讨论】:

  • 一种简洁紧凑的方法,但我会犹豫在任何对性能过于敏感的 throw/catch 中使用它,这是 Ruby 中一种昂贵的流控制形式。
猜你喜欢
  • 1970-01-01
  • 2016-03-24
  • 2019-10-31
  • 2016-10-31
  • 2021-04-28
  • 2011-02-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多