【问题标题】:Why is a string key for a hash frozen?为什么哈希的字符串键被冻结?
【发布时间】:2012-10-14 05:38:07
【问题描述】:

根据specification,用作哈希键的字符串被复制和冻结。其他可变对象似乎没有这种特殊考虑。例如,使用数组键,以下是可能的。

a = [0]
h = {a => :a}
h.keys.first[0] = 1
h # => {[1] => :a}
h[[1]] # => nil
h.rehash
h[[1]] # => :a

另一方面,类似的事情不能用字符串键来完成。

s = "a"
h = {s => :s}
h.keys.first.upcase! # => RuntimeError: can't modify frozen String

当涉及到哈希键时,为什么将字符串设计为与其他可变对象不同?是否有任何用例可以使该规范变得有用?该规范还有哪些其他后果?


我实际上有一个用例,其中缺少关于字符串的这种特殊规范可能很有用。也就是说,我使用 yaml gem 读取了一个手动编写的描述哈希的 YAML 文件。键可能是字符串,我想在原始 YAML 文件中允许不区分大小写。当我读取文件时,我可能会得到这样的哈希:
h = {"foo" => :foo, "Bar" => :bar, "BAZ" => :baz}

我想将键规范化为小写以获得这个:

h = {"foo" => :foo, "bar" => :bar, "baz" => :baz}

通过这样做:

h.keys.each(&:downcase!)

但由于上述原因返回错误。

【问题讨论】:

  • 看起来,就我而言,我能做的最好的事情是h.keys.each{|s| h.store(s.downcase, h.delete(s))}。
  • 我只能猜测“为什么”。除了字符串是比数组更常见的用例外,我怀疑冻结字符串会更容易实现。如果我知道 Perl,我会看看 Ruby 是否试图在哈希行为上与 Perl 保持一致。如果我精通日语,我会查看何时实施冻结键,看看这是否是错误报告或邮件列表上讨论的结果(大概是日语,因为在 Ruby 的历史上如此早期的事情)。
  • @AndrewGrimm Here 它说数组和散列不能成为散列的好键,因为它们可以被修改,并且字符串被冻结,因此您不必调用 rehash。与 steenslag 的回答一致。
  • 不确定这个方法是什么时候添加的,transform_keys!可以做h.transform_keys!(&:downcase)

标签: ruby string hash mutable


【解决方案1】:

简而言之,只是 Ruby 试图变得更好。

当在 Hash 中输入一个键时,会使用该键的 hash 方法计算一个特殊数字。 Hash 对象使用这个数字来检索密钥。例如,如果您询问h['a'] 的值是什么,哈希调用字符串'a' 的hash 方法并检查它是否为该数字存储了一个值。当某人(你)改变字符串对象时,问题就出现了,所以字符串'a'现在是别的东西,比如说'aa'。哈希找不到“aa”的哈希数。

最常见的散列键类型是字符串、符号和整数。符号和整数是不可变的,但字符串不是。 Ruby 试图通过复制和冻结字符串键来保护您免受上述令人困惑的行为。我想其他类型没有这样做,因为可能会有令人讨厌的性能副作用(想想大型数组)。

【讨论】:

  • 感谢您回答问题的理论部分。
【解决方案2】:

不可变的键通常是有意义的,因为它们的哈希码是稳定的。

这就是为什么在这部分 MRI 代码中对字符串进行特殊转换的原因:

if (RHASH(hash)->ntbl->type == &identhash || rb_obj_class(key) != rb_cString) {
  st_insert(RHASH(hash)->ntbl, key, val);
}
else {
  st_insert2(RHASH(hash)->ntbl, key, val, copy_str_key);
}

简而言之,在字符串键的情况下,st_insert2 被传递一个指向将触发复制和冻结的函数的指针。

因此,如果我们理论上想要支持不可变列表和不可变哈希作为哈希键,那么我们可以将代码修改为如下所示:

VALUE key_klass;
key_klass = rb_obj_class(key);
if (key_klass == rb_cArray || key_klass == rb_cHash) {
  st_insert2(RHASH(hash)->ntbl, key, val, freeze_obj);
}
else if (key_klass == rb_cString) {
  st_insert2(RHASH(hash)->ntbl, key, val, copy_str_key);
}
else {
  st_insert(RHASH(hash)->ntbl, key, val);
}

freeze_obj 将被定义为:

static st_data_t
freeze_obj(st_data_t obj)
{
    return (st_data_t)rb_obj_freeze((VALUE) obj);
}

这样就可以解决您观察到的特定不一致问题,即数组键是可变的。然而,要真正保持一致,还需要将更多类型的对象设为不可变。

不是所有类型,但是。例如,冻结像 Fixnum 这样的直接对象是没有意义的,因为实际上只有一个 Fixnum 实例对应于每个整数值。这就是为什么只有 String 需要以这种方式特殊,而不是 Fixnum 和 Symbol。

字符串是一个特殊的例外,只是为了方便 Ruby 程序员,因为字符串经常被用作哈希键。

相反,其他对象类型没有像这样冻结的原因,诚然会导致不一致的行为,这主要是为了方便 Matz & Company 不支持边缘情况。在实践中,很少有人会使用像数组或散列这样的容器对象作为散列键。因此,如果您这样做,您可以在插入前冻结。

请注意,这与性能无关,因为冻结非立即对象的行为仅涉及翻转每个对象上存在的basic.flags 位域上的FL_FREEZE 位。这当然是一种廉价的操作。

另外谈到性能,请注意,如果您要使用字符串键,并且您处于代码的性能关键部分,您可能希望在插入之前冻结您的字符串。如果不这样做,则会触发 dup,这是一个更昂贵的操作。

更新 @sawa 指出,让您的数组键简单地冻结意味着原始数组可能在键使用上下文之外出乎意料地不可变,这也可能是一个令人不快的惊喜(尽管它真的可以为您提供使用数组作为哈希键的权利)。因此,如果您推测 dup + freeze 是解决此问题的方法,那么您实际上可能会产生明显的性能成本。另一方面,让它完全解冻,你会得到 OP 的原始怪异。诡异无处不在。 Matz 等人将这些边缘情况交给程序员的另一个原因。

【讨论】:

  • 冻结原始密钥而不复制它会令人困惑。如果要自动冻结密钥,则必须进行复制。即使冻结很便宜,复制阵列等也很昂贵,所以这似乎是一个性能问题。你的最后一段内容丰富。你确定,如果一个字符串从一开始就被冻结,它在用作哈希键时不会被复制?
  • 至于确定它是否是这样工作的,是的,你可以在这里看到它:if (OBJ_FROZEN(orig)) return orig; 在rb_str_new_frozen() 的顶部,当前位于:github.com/ruby/ruby/blob/树干/string.c#L673
  • 我不一定同意“复制是必须的”......如果设置哈希键的一致行为是它们都被简单地冻结了,那么做不寻常事情的人会尝试使用数组作为键,然后对其进行变异,当更新尝试大声失败时,很快就会发现这种用法不起作用。一致性有时可能会有所帮助。现在,我肯定也看到了你来自哪里......只是优化什么似乎是有争议的——一致性、性能、保护程序员免受做奇怪事情的后果等。
【解决方案3】:

请参阅 this thread on the ruby-core mailing list 以获取解释(奇怪的是,这恰好是我在邮件应用程序中打开邮件列表时偶然发现的第一封邮件!)。

我不知道你问题的第一部分,但是 h这是第二部分的实用答案:

  new_hash = {}
  h.each_pair do |k,v|
   new_hash.merge!({k.downcase => v}) 
  end

  h.replace new_hash

这种代码有很多排列方式,

  Hash[ h.map{|k,v| [k.downcase, v] } ]

作为另一个人(你可能已经意识到这些,但有时最好采取实际的路线:)

【讨论】:

    【解决方案4】:

    您提出 2 个不同的问题:理论和实践。 Lain 是第一个回答的人,但我想为您的实际问题提供一个我认为合适、更懒惰的解决方案:

    Hash.new { |hsh, key| # this block get's called only if a key is absent
      downcased = key.to_s.downcase
      unless downcased == key # if downcasing makes a difference
        hsh[key] = hsh[downcased] if hsh.has_key? downcased # define a new hash pair
      end # (otherways just return nil)
    }
    

    与Hash.new 构造函数一起使用的块仅针对那些缺少的键调用,这些键实际上是请求的。上述解决方案也接受符号。

    【讨论】:

      【解决方案5】:

      一个非常古老的问题 - 但如果其他人试图回答“我怎样才能绕过哈希键正在冻结字符串”这个问题的一部分......

      解决字符串特殊情况的一个简单技巧是:

      class MutableString < String
      end
      
      s = MutableString.new("a")
      h = {s => :s}
      h.keys.first.upcase! # => RuntimeError: can't modify frozen String
      puts h.inspect
      

      除非您正在创建密钥,否则它不起作用,并且除非您小心,否则它不会对严格要求类完全为“字符串”的任何内容造成任何问题

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-07-30
        • 2012-06-20
        • 2015-01-01
        • 1970-01-01
        • 2013-11-21
        • 1970-01-01
        • 2013-05-02
        • 1970-01-01
        相关资源
        最近更新 更多