Ruby中通过递归实现的嵌套哈希

4
我一直在尝试以编程方式创建Ruby中的嵌套默认哈希表,基本上是Ruby中冒号的简写:
h = Hash.new {|h,k| h[k] = Hash.new}

我希望把这个功能扩展到尽可能多的层级。我写了下面这个函数:

def nested_hash(level, default={})
   return default if level == 0
   return Hash.new{ |h,k| h[k] = nested_hash(level - 1, default) }
end

看起来它正常工作,但是当我创建多个密钥时,遇到了以下问题

h = nested_hash(1)
h[0][1] = [1, 2, 3] # h is {0=>{1=>[1, 2, 3]}}
h[2] # should give a new Hash, but returns {1=>[1, 2, 3]}
h # {0=>{1=>[1, 2, 3]}, 2=>{1=>[1, 2, 3]}} 

为什么函数的默认值会改变并变为先前设置的值?
编辑
我已经找到了一个可行的解决方案:
def nested_hash(level, default={})
    return Hash.new{ |h,k| h[k] = default } if level <= 1
    Hash.new{ |h,k| h[k] = nested_hash(level - 1, default) }
end

不要紧,这种方法也无法解决类似的问题:
h = nested_hash(1)
h[0][1] = [1, 2, 3]
h[2][0] # nil
h # {0=>{1=>[1, 2, 3]}, 2=>{1=>[1, 2, 3]}}

我仍然不明白为什么原始默认值要在键之间共享。

2个回答

4

仅出于好奇:

hash =
  Hash.new do |h, k|
    h[k] = h.dup.clear.extend(Module.new do
      define_method(:level, ->{ h.level - 1 })
    end).tap { |this| raise "" if this.level <= 0 }
  end.extend(Module.new { define_method(:level, ->{ 5 }) })

#⇒ {}

hash["0"]["1"]["2"]["3"]
#⇒ {}
hash["0"]["1"]["2"]["3"]["4"]
#⇒ RuntimeError: ""

或者,作为一个函数:
def nhash lvl
  Hash.new do |h, k|
    h[k] = h.dup.clear.extend(Module.new do
      define_method(:level, ->{ h.level - 1 })
    end).tap { |this| raise "" if this.level < 0 }
  end.extend(Module.new { define_method(:level, ->{ lvl }) })
end

导致:
✎ h = nhash 2
#⇒ {}
✎ h[0][1] = [1, 2, 3]
#⇒ [1, 2, 3]
✎ h[2][0]
#⇒ {}
✎ h[2][0][5]
#⇒ RuntimeError: 

如果需要,可以重置默认的处理程序而不是抛出异常。

这种方法唯一的缺陷在于,尝试调用许可级别以上的层级时,所有中间的空哈希都将被创建。通过定义方法、累积路径(而不仅仅返回级别)并在抛出异常前清除空父级,也可以克服这个问题。


很棒的答案。但我想知道为什么需要 h.dup.clear 而不是一个新的空哈希? - Mulan
哦,也许是为了继承动态的:level方法?非常有趣的方法 :D - Mulan
我们能否扩展它以具有默认值?理想情况下,我希望这也适用于将空数组作为键。类似于 h = nhash(2, []) 这样的东西,之后我可以执行 h[0][1] << 1。或者甚至像 h = nhash(2, 0) 这样的东西,之后我可以执行 h[0][1] += 1 - Abundance
“我们可以扩展吗[...]?” - 当然可以。 - Aleksei Matiushkin

2
您可以使用default_proc来实现此功能。文档中最好的解释如下:

如果Hash::new被调用时带有一个块,则返回该块,否则返回nil。

因此,在这种情况下,每个新的哈希键都会使用其父哈希的默认值进行实例化。
例如:
hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }

这样,每个嵌套级别都将具有相同的默认设置:
hash[0][1] = [1, 2, 3]
hash[:first_level] # => {}
hash[:first_level][:second_level] # => {}
hash[2] # => {}
hash # => {0=>{1=>[1, 2, 3]}, :first_level=>{:second_level=>{}}, 2=>{}}

在这种情况下,您可以使用一些丑陋但有效的方法,例如:
hash = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = raise('Too Deep') } } }
hash[1] # => {}
hash[1][2] # => {}
hash[1][2][3] # => RuntimeError: Too Deep

这确实很酷,但我想为级别设置一个硬截止。hash[0][1][2][3]会给出一个空哈希,而我想让它报错。 - Abundance
感谢更新@Abundance - 被“我想将其扩展到尽可能多的级别”引入歧途。看到你已经找到了一个好的解决方案,很高兴你成功了。 - SRack
@SRack 你可能会感兴趣:我在我的回答中稍微扩展了你的想法,使长度动态配置 :) - Aleksei Matiushkin
1
感谢您的评论@AlekseiMatiushkin,这是一个非常好的答案。很高兴您在这里分享,否则我可能会错过它 :) - SRack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接