Подтвердить что ты не робот

Почему строковый ключ для хеша заморожен?

В соответствии с спецификацией строки, которые используются в качестве ключа к хэшу, дублируются и заморожены. Другие изменчивые объекты, похоже, не имеют такого особого рассмотрения. Например, с помощью ключа массива возможно следующее.

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

но возвращает ошибку по причине, описанной выше.

4b9b3361

Ответ 1

Короче говоря, просто Ruby пытается быть приятным.

Когда ключ вводится в хеш, вычисляется специальный номер, используя метод hash ключа. Объект Hash использует этот номер для извлечения ключа. Например, если вы спросите, что такое значение h['a'], то Hash вызывает метод hash строки 'a' и проверяет, имеет ли оно значение, сохраненное для этого числа. Проблема возникает, когда кто-то (вы) мутирует строковый объект, поэтому строка "a" теперь является чем-то другим, скажем "aa". Хэш не найдет номер хэша для 'aa'.

Наиболее распространенными типами ключей для хэшей являются строки, символы и целые числа. Символы и целые числа неизменяемы, но строк нет. Ruby пытается защитить вас от запутанного поведения, описанного выше, путем дублирования и замораживания строк. Я предполагаю, что это не сделано для других типов, потому что могут быть неприятные побочные эффекты производительности (подумайте о больших массивах).

Ответ 2

См. этот поток в списке рассылки ruby-core для объяснения (причудливо, это была первая почта, на которую я наткнулся, когда я открыл список рассылки в моем почтовом приложении!).

Я понятия не имею о первой части вашего вопроса, но 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] } ]

является другим (и вы, вероятно, знаете об этом, но иногда лучше всего использовать практический маршрут:)

Ответ 3

Неизменяемые ключи имеют смысл вообще, потому что их хэш-коды будут стабильными.

Вот почему строки были специально преобразованы в эту часть кода 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, чтобы не поддерживать краевые случаи. На практике сравнительно мало людей будут использовать объект-контейнер, например массив или хеш, как хэш-ключ. Поэтому, если вы сделаете это, вам придется замораживать перед вставкой.

Обратите внимание, что это не строго касается производительности, потому что действие замораживания объекта немедленного просто включает в себя щелкнуть бит FL_FREEZE в битовом поле basic.flags, который присутствует на каждом объекте. Это, конечно, дешевая операция.

Также, говоря о производительности, обратите внимание, что если вы собираетесь использовать строковые ключи, и вы находитесь в критическом для производительности разделе кода, вы можете заблокировать свои строки перед выполнением вставки. Если вы этого не сделаете, произойдет дублирование, что является более дорогостоящей операцией.

Update @sawa указала, что оставляя ваш массив-ключ просто замороженным, означает, что исходный массив может быть неожиданно неизменным вне контекста использования ключа, что также может быть неприятным сюрпризом (хотя от него будет служить вам для использования массива как хэш-ключа, действительно). Если вы, следовательно, предположите, что это просто-напросто, то вы бы на самом деле понесли возможную заметную стоимость исполнения. С другой стороны, оставьте его полностью размороженным, и вы получите оригинальную странность OP. Странность вокруг. Еще одна причина, по которой Мац и др. Отложить эти краевые случаи программисту.

Ответ 4

Вы задаете два разных вопроса: теоретические и практические. Лейн первым ответил, но я хотел бы предоставить то, что считаю правильным, более ленивым решением вашего практического вопроса:

Hash.new { |hsh, key| # this block get 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, вызывается только для тех отсутствующих ключей, которые действительно запрашиваются. Вышеупомянутое решение также принимает символы.