为什么哈希冻结的字符串键?

时间:2012-10-24 07:44:52

标签: ruby string hash mutable

根据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!)

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

4 个答案:

答案 0 :(得分:22)

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

在哈希中输入密钥时,使用密钥的hash方法计算特殊编号。 Hash对象使用此数字来检索密钥。例如,如果你问h['a']的值是什么,Hash会调用字符串'a'的hash方法,并检查它是否有为该数字存储的值。当有人(你)改变字符串对象时会出现问题,所以字符串'a'现在是别的东西,让我们说'aa'。哈希不会找到'aa'的哈希值。

哈希的最常见键类型是字符串,符号和整数。符号和整数是不可变的,但字符串不是。 Ruby试图通过加密和冻结字符串键来保护您免受上述混淆行为的影响。我想这对其他类型没有做,因为可能有令人讨厌的性能副作用(想想大型数组)。

答案 1 :(得分:4)

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

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

  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] } ]

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

答案 2 :(得分:4)

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

这就是在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传递一个指向函数的指针,该函数将触发dup并冻结。

因此,如果我们理论上希望支持不可变列表和不可变哈希作为哈希键,那么我们可以将代码修改为这样的代码:

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需要以这种方式进行特殊设置,而不是FixnumSymbol

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

相反,其他对象类型这样冻结的原因,这无疑会导致行为不一致,这主要是为了方便Matz&amp;公司不支持边缘案件。实际上,相对较少的人会使用像数组或散列之类的容器对象作为散列键。因此,如果您这样做,则由您在插入之前冻结。

请注意,这并不是严格意义上的性能,因为冻结非直接对象的行为只涉及翻转每个对象上存在的FL_FREEZE位域上的basic.flags位。那当然是一种廉价的操作。

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

更新 @sawa指出,将数组密钥简单地冻结意味着原始数组可能在密钥使用上下文之外意外地不可变,这也可能是一个令人不快的惊喜(尽管它是otoh它会为你提供使用数组作为哈希键的权利。因此,如果你猜测dup + freeze就是这样的话,那么你实际上可能会产生可观的性能成本。第三方面,让它完全解冻,你得到OP的原始怪异。周围的怪异。 Matz等人将这些边缘情况推迟给程序员的另一个原因。

答案 3 :(得分: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构造函数一起使用的块。上述解决方案也接受符号。