不可变键通常是有意义的,因为它们的哈希码是稳定的。
这就是为什么在这部分 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
and Symbol
。
字符串是一个特殊的例外,只是为了方便 Ruby 程序员,因为字符串经常用作哈希键。
相反,其他对象类型没有像这样冻结的原因,诚然会导致不一致的行为,这主要是为了方便 Matz & Company 不支持边缘情况。在实践中,很少有人会使用像数组或散列这样的容器对象作为散列键。因此,如果您这样做,则在插入之前由您决定冻结。
请注意,这并不是严格意义上的性能,因为冻结非立即对象的行为仅涉及翻转每个对象上存在的位域FL_FREEZE
上的位。basic.flags
这当然是一种廉价的操作。
还谈到性能,请注意,如果您要使用字符串键,并且您处于代码的性能关键部分,您可能希望在插入之前冻结您的字符串。如果不这样做,则会触发 dup,这是一个更昂贵的操作。
更新@sawa 指出,将您的数组键简单地冻结意味着原始数组可能在键使用上下文之外出乎意料地不可变,这也可能是一个令人不快的惊喜(尽管它可以为您提供正确的使用数组作为哈希键,真的)。如果您因此推测 dup + freeze 是解决此问题的方法,那么您实际上可能会招致明显的性能成本。另一方面,让它完全解冻,你会得到 OP 的原始怪异。诡异无处不在。Matz 等人将这些边缘情况推迟到程序员的另一个原因。