Why is a string key for a hash frozen?

前端 未结 4 1517
猫巷女王i
猫巷女王i 2021-02-07 00:52

According to the specification, strings that are used as a key to a hash are duplicated and frozen. Other mutable objects do not seem to have such special consideration. For exa

4条回答
  •  [愿得一人]
    2021-02-07 01:35

    Immutable keys make sense in general because their hash codes will be stable.

    This is why strings are specially-converted, in this part of MRI code:

    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);
    }
    

    In a nutshell, in the string-key case, st_insert2 is passed a pointer to a function that will trigger the dup and freeze.

    So if we theoretically wanted to support immutable lists and immutable hashes as hash keys, then we could modify that code to something like this:

    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);
    }
    

    Where freeze_obj would be defined as:

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

    So that would solve the specific inconsistency that you observed, where the array-key was mutable. However to be really consistent, more types of objects would need to be made immutable as well.

    Not all types, however. For example, there'd be no point to freezing immediate objects like Fixnum because there is effectively only one instance of Fixnum corresponding to each integer value. This is why only String needs to be special-cased this way, not Fixnum and Symbol.

    Strings are a special exception simply as a matter of convenience for Ruby programmers, because strings are very often used as hash keys.

    Conversely, the reason that other object types are not frozen like this, which admittedly leads to inconsistent behavior, is mostly a matter of convenience for Matz & Company to not support edge cases. In practice, comparatively few people will use a container object like an array or a hash as a hash key. So if you do so, it's up to you to freeze before insertion.

    Note that this is not strictly about performance, because the act of freezing a non-immediate object simply involves flipping the FL_FREEZE bit on the basic.flags bitfield that's present on every object. That's of course a cheap operation.

    Also speaking of performance, note that if you are going to use string keys, and you are in a performance-critical section of code, you might want to freeze your strings before doing the insertion. If you don't, then a dup is triggered, which is a more-expensive operation.

    Update @sawa pointed out that leaving your array-key simply frozen means the original array might be unexpectedly immutable outside of the key-use context, which could also be an unpleasant surprise (although otoh it would serve you right for using an array as a hash-key, really). If you therefore surmise that dup + freeze is the way out of that, then you would in fact incur possible noticeable performance cost. On the third hand, leave it unfrozen altogether, and you get the OP's original weirdness. Weirdness all around. Another reason for Matz et al to defer these edge cases to the programmer.

提交回复
热议问题