如何在Ruby哈希中动态设置嵌套键的值

4

这个应该很简单,但我找不到一个合适的解决方案。 对于第一级键:

resource.public_send("#{key}=", value)

但是对于 foo.bar.lolo 呢?

我知道可以按照以下方式获取:

'foo.bar.lolo'.split('.').inject(resource, :send)

或者
resource.instance_eval("foo.bar.lolo")

但是如果我不知道嵌套的层级,无论是第二层还是第三层,该如何将值设置为最后一个变量呢?

是否有一般的方法适用于所有层级? 对于我的例子,我可以这样做:

resource.public_send("fofo").public_send("bar").public_send("lolo=", value)

可能的重复:https://dev59.com/L1sW5IYBdhLWcg3wwZcPhttps://dev59.com/N57ha4cB1Zd3GeqPfS_5 - Rene Wooller
5个回答

5

关于哈希,只是出于好奇:

hash = { a: { b: { c: 1 } } }
def deep_set(hash, value, *keys)
  keys[0...-1].inject(hash) do |acc, h|
    acc.public_send(:[], h)
  end.public_send(:[]=, keys.last, value)
end

deep_set(hash, 42, :a, :b, :c)
#⇒ 42
hash
#⇒ { a: { b: { c: 42 } } }

1
这不就是与hash#bury基本相同吗? 即 这个 - max pleaner
@maxple 我不知道 bury 是什么意思。这是纯粹的 Ruby。 - Aleksei Matiushkin
@maxple 我认为你应该将那个链接和一个使用它的例子添加为答案。在询问外部资源方面是离题的,但在答案中提供它们不是离题的。 - Wayne Conrad
@maxpleaner,它与您的bury实现不同,因为它无法处理在哈希键上设置不存在的值。 - Rene Wooller
注意:如果您想在没有预定义键的哈希上使用此功能,您需要修改哈希的default_proc,例如将以下内容添加到上面函数的第一行:hash.default_proc = proc { |h,k| h[k] = Hash.new(&h.default_proc) } - Rene Wooller
显示剩余2条评论

2
在Ruby中,默认情况下哈希表(Hashes)不提供这些点方法。你可以链接send调用(这适用于任何对象,但通常不能以这种方式访问哈希键):
  "foo".send(:downcase).send(:upcase)

在处理嵌套的哈希表时,可变性的概念是一个棘手的问题。例如:

  hash = { a: { b: { c: 1 } } }
  nested = hash[:a][:b]
  nested[:b] = 2
  hash
  # => { a: { b: { c: 2 } }

这里的“可变性”是指,当您将嵌套哈希存储到单独的变量中时,它仍然实际上是指向原始哈希的指针。如果您不理解它,可变性在某些情况下很有用,但也可能会创建错误。

您可以将:a:b分配给变量,以使其在某种意义上变得“动态”。

还有更高级的方法来做到这一点,例如在较新的Ruby中使用dig

versions.

  hash = { a: { b: { c: 1 } } }
  keys_to_get_nested_hash = [:a, :b]
  nested_hash = hash.dig *keys_to_get_nested_hash
  nested_hash[:c] = 2
  hash
  # => { a: { b: { c: 2 } } }

如果您使用OpenStruct,则可以为哈希值提供点方法访问器。说实话,我并不经常使用链接send调用。如果它有助于您编写代码,那太好了。但是您不应该发送用户生成的输入,因为这是不安全的。

2
hash.public_send(:[], :a).public_send(:[]=, :b, 42) 完美地运行,没有什么棘手的。 - Aleksei Matiushkin
@AlekseiMatiushkin 在这里,版本3.0.3中我发现它只有在您预定义哈希键时才起作用,例如: {}.public_send(:[], :a).public_send(:[]=, :b, 42) 会得到 undefined method `[]=' for nil:NilClass - Rene Wooller

0
假设已知存在这些键,那么使用Hash#dig可以得到一个更简洁的解决方案。
hash = { a: { b: { c: 1 } } }

def deep_set(hash, value, *keys)
  hash.dig(*keys[0..-2])[keys[-1]] = value
end

deep_set(hash, 42, :a, :b, :c)
#⇒ 42

hash
#⇒ { a: { b: { c: 42 } } }

这只是示例代码。如果键不被知晓或者deep_set接收到少于两个键,它将无法工作。这两个问题都是可以解决的,但超出了提问者的问题范围。

0

如果您希望允许初始化缺少的嵌套键,我建议对Aleksei Matiushkin' solution进行以下重构。

我不想对实际答案进行更改,因为它本身就是完全有效的,而这只是引入了额外的东西。

hash = { a: {} } # missing some nested keys
def deep_set(hash, value, *keys)
  keys[0...-1].inject(hash) do |acc, h|
    acc[h] ||= {} # initialize the missing keys (ex: b in this case) 
    acc.public_send(:[], h)
  end.public_send(:[]=, keys.last, value)
end

deep_set(hash, 42, :a, :b, :c)
#⇒ 42
hash
#⇒ { a: { b: { c: 42 } } }

0

虽然您可以实现一些方法来按照您现在设置的方式完成某些操作,但我强烈建议您重新考虑您的数据结构。

为了澄清一些术语,您示例中的key不是一个键,而是一个方法调用。在Ruby中,当您有像my_thing.my_other_thing这样的代码时,my_other_thing始终是一个方法,而不是一个键,至少不是在术语的正确意义上。

确实,您可以通过这种方式链接对象来创建类似哈希的结构,但这里存在真正的代码异味。如果您将foo.bar.lolo视为在哈希中查找嵌套的lolo键的方法,则应该使用常规哈希表。

x = {foo: {bar: 'lolo'}}
x[:foo][:bar] # => 'lolo'
x[:foo][:bar] = 'new_value' # => 'new_value'

另外,虽然可以使用send/instance_eval方法实现这种方式,但这并不是最佳做法,甚至可能会产生安全问题。


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