有没有一种轻量级的方法来锁定哈希上的一组键?

Is there a lightweight way to lock down a set of keys on a Hash?

需要说明的是,我非常乐意自己将此功能作为自定义 class 实现,但我想确保我没有忽略 ruby 或 rails 魔法。我用谷歌搜索了关键字 "ruby rails hash keys values immutable lock freeze" 的每一个有意义的排列。但到目前为止运气不好!


问题:我需要给一个Hash一组key,可能在运行的时候,然后在不加锁的情况下锁定这组key他们的价值观。类似于以下内容:

to_lock = {}
to_lock[:name] = "Bill"
to_lock[:age] = 42

to_lock.freeze_keys     # <-- this is what I'm after, so that:

to_lock[:name] = "Bob"  # <-- this works fine,
to_lock[:height]        # <-- this returns nil, and
to_lock[:height] = 175  # <-- this throws some RuntimeError

问题:是否有一些 ruby 或 rails 工具允许这样做?


我知道 Object#freeze and of Immutable::Hash,但是锁定键 值。

坚持开箱即用 ruby,可以通过在 运行 时间操纵 classes 的方法或访问器来满足用例的大部分需求,如this or this, then overriding #method_missing。但这感觉有点笨拙。这些技术也不是真正的 "lock" 方法或访问器集,添加更多只是很尴尬。到那时,最好简单地编写一个 class 来完全实现上面的代码片段并根据需要进行维护。

您可以通过为您的 "to-lock" 哈希实例定义自定义 []= 来实现此目的, 添加允许的键之后:

x = { name: nil, age: nil }

def x.[]=(key, value)
  # blow up unless the key already exists in the hash
  raise 'no' unless keys.include?(key)
  super
end

x[:name] # nil
x[:name] = "Bob" # "Bob"

x[:size] # nil
x[:size] = "large" # raise no

请注意,这不会阻止您使用 merge!.

之类的内容无意中添加密钥

@meagar 提供了一个有趣的解决方案,但指出它仅在尝试使用 Hash#[] 添加键值对时有效。此外,它不会阻止密钥被删除。

这是另一种方法,但它相当笨拙,所以我认为您可能应该寻找一种不同的方法来 skin your cat

class Hash
  def frozen_keys_create
    self.merge(self) { |*_,v| [v] }.freeze
  end

  def frozen_keys_get_value(k)
    self[k].first
  end

  def frozen_keys_put_value(k, new_value)
    self[k].replace [new_value]
    self
  end

  def frozen_keys_to_unfrozen
    self.merge(self) { |*_,v| v.first }
  end
end

现在让我们开始使用它们。

创建一个冻结散列,每个值都封装在一个数组中

sounds = { :cat=>"meow", :dog=>"woof" }.frozen_keys_create
  #=> {:cat=>["meow"], :dog=>["woof"]}
sounds.frozen?
  #=> true

这会阻止添加密钥:

sounds[:pig] = "oink"
  #=> RuntimeError: can't modify frozen Hash
sounds.update(:pig=>"oink")
  #=> RuntimeError: can't modify frozen Hash

或删除:

sounds.delete(:cat)
  #=> RuntimeError: can't modify frozen Hash
sounds.reject! { |k,_| k==:cat }
  #=> RuntimeError: can't modify frozen Hash

获取一个值

sounds.frozen_keys_get_value(:cat)
  #=> "meow"

更改值

sounds.frozen_keys_put_value(:dog, "oooooowwwww")
  #=> {:cat=>["meow"], :dog=>["oooooowwwww"]}

转换为键未冻结的散列

new_sounds = sounds.frozen_keys_to_unfrozen
  #=> {:cat=>"meow", :dog=>"oooooowwwww"} 
new_sounds.frozen?
  #=> false 

添加和删除键

甚至可以添加(私有的,也许)方法来添加或删除键以覆盖所需的行为。

class Hash
  def frozen_keys_add_key_value(k, value)
    frozen_keys_to_unfrozen.tap { |h| h[k] = value }.frozen_keys_create
  end

  def frozen_keys_delete_key(k)
    frozen_keys_to_unfrozen.reject! { |key| key == k }.frozen_keys_create
  end
end

sounds = { :cat=>"meow", :dog=>"woof" }.frozen_keys_create
  #=> {:cat=>["meow"], :dog=>["oooowwww"]}
new_sounds = sounds.frozen_keys_add_key_value(:pig, "oink")
  #=> {:cat=>["meow"], :dog=>["woof"], :pig=>["oink"]} 
new_sounds.frozen?
  #=> true 
newer_yet = new_sounds.frozen_keys_delete_key(:cat)
  #=> {:dog=>["woof"], :pig=>["oink"]} 
newer_yet.frozen?
  #=> true 

听起来像是内置 Struct

的一个很好的用例
irb(main):001:0> s = Struct.new(:name, :age).new('Bill', 175)
=> #<struct name="Bill", age=175>
irb(main):002:0> s.name = 'Bob'
=> "Bob"
irb(main):003:0> s.something_else
NoMethodError: undefined method `something_else' for #<struct name="Bob", age=175>
    from (irb):3
    from /home/jtzero/.rbenv/versions/2.3.0/bin/irb:11:in `<main>'