Объект Ruby Hash - это ассоциативная структура данных, используемая для хранения пар "ключ-значение". Во многих языках есть объекты, которые служат схожей цели: в Python есть словари, в JavaScript есть карты, в Java есть HashMaps и так далее. Объект Ruby Hash довольно прост в использовании.
my_hash = {} my_hash[0] = 'Hello!' puts my_hash[0] #=> Hello!
Это прекрасно работает. Мы также можем изменить значение, связанное с ключом, который уже существует в хэше.
my_hash[0] << "I've been mutated!" puts my_hash[0] #=> Hello! I've been mutated!
Оператор []
- это просто синтаксический сахар для метода #[]
, определенного в классе Hash. Вот описание из Ruby Docs:
hsh [ключ] → значение
«Извлекает значение объекта, соответствующего ключевому объекту . Если не найден, возвращает значение по умолчанию. »
Последнее предложение в этом описании заслуживает более подробного пояснения.
my_hash = {} my_hash[:exists] = 0 my_hash[:exists] += 1 puts my_hash[:exists] #=> 1 my_hash[:does_not_exist] += 1 #=> NoMethodError (undefined method `+' for nil:NilClass)
У хэшей есть значение по умолчанию, которое является значением, возвращаемым при доступе к ключам, которых нет в хэше. В приведенном выше коде мы пытаемся получить доступ к ключу :does_not_exist
, а затем вызвать метод #+
для значения, связанного с этим ключом. Сообщение об ошибке сообщает нам, что не существует #+
метода, определенного для значения, которое возвращается при доступе к этому ключу. Это связано с тем, что хэши, инициализированные с использованием кода hash_name = {}
, будут иметь значение по умолчанию, равное nil
.
p my_hash[:does_not_exist] #=> nil
Мы можем сами установить значение по умолчанию, используя метод Hash::new
:
my_hash = Hash.new(0) my_hash[:does_not_exist] += 1 puts my_hash[:does_not_exist] #=> 1
Ничто из этого не особо впечатляет, но что произойдет, если мы попытаемся запустить этот код:
my_hash = Hash.new([]) my_hash[:does_not_exist] << 'Uh oh.' p my_hash #=> {} p my_hash[:does_not_exist] #=> ["Uh oh."]
Подождите минуту. Метод p
использует метод #inspect
для печати удобочитаемого представления переданного объекта. Так что вы, возможно, ожидали увидеть {:does_not_exist=>["Uh oh."]}
, но вместо этого похоже, что хеш все еще пуст. Но если мы получим значение, связанное с символом :does_not_exist
(который, как следует из его названия, не существует), мы получим ожидаемое значение.
p my_hash[:does_not_exist_either] #=> ["Uh oh."]
Хорошо, это странно. Похоже, в хеше есть пары "ключ-значение", которые мы даже не указали.
На самом деле этому есть разумное объяснение, но оно требует внимательного отношения к описанию метода Hash::new
в Ruby Docs.
new → new_hash
новый (объект) → новый_хэш
Возвращает новый пустой хеш. Если к этому хешу впоследствии обращается ключ, который не соответствует хеш-записи, возвращаемое значение зависит от стиля
new
, используемого для создания хеша. В первой форме доступ возвращаетnil
. Если указан obj , этот единственный объект будет использоваться для всех значений по умолчанию .
Последнее предложение является ключевым. Когда мы инициализируем хэш с помощью Hash.new([])
, мы передаем объект массива. Этот одиночный объект будет использоваться как значение по умолчанию для хэша. Мы можем доказать это, напечатав несколько идентификаторов объектов.
my_hash = Hash.new([]) my_hash[0] << 1 puts my_hash[0].object_id #=> 180 puts my_hash[:does_not_exist].object_id #=> 180
Когда мы используем оператор <<
во второй строке этого кода, мы вообще не изменяем хэш. Фактически, мы изменяем значение по умолчанию, которое возвращает метод Hash#[]
, когда мы пытаемся получить доступ к несуществующему ключу. Внимательно подумайте, что происходит в этом коде:
Строка 1: мы вызываем метод Hash::new
, передавая в качестве аргумента пустой объект массива. Это возвращает новый хэш-объект, который будет использовать пустой объект массива в качестве значения по умолчанию.
Строка 2: мы используем оператор []
в хэше, который является синтаксическим сахаром для метода Hash#[]
. Мы передаем целое число 0
в качестве аргумента для этого метода. Поскольку в нашем хэше нет ключа со значением 0
, метод возвращает значение по умолчанию (наш единственный пустой объект массива). Затем мы используем оператор <<
для вызова метода Array#<<
для изменения этого пустого объекта массива, не хеша! Мы передаем целое число со значением 1
в метод #<<
, который добавляет это целое число в конец нашего ранее пустого массива. Ключевым моментом здесь является то, что наш хеш остается неизменным этим кодом.
Рассмотрим этот код:
my_array = [] my_hash = Hash.new(my_array) my_hash[0] << 1 p my_hash #=> {} p my_array #=> [1] p my_array.object_id #=> 200 p my_hash[:does_not_exist].object_id #=> 200
Этот код в основном такой же, как и предыдущий фрагмент кода, за исключением того, что он должен прояснить, что мутируется наше значение по умолчанию, а не хеш.
То, что мы здесь обнаружили, характерно не только для массивов как хеш-значений. Эта же концепция применима ко всем изменяемым объектам.
my_string = '' my_hash = Hash.new(my_string) my_hash[0] << 'nope.' p my_hash #=> {} p my_string #=> "nope."
Итак, есть ли другой способ заставить эту работу работать? Можем ли мы использовать изменяемый объект в качестве значения по умолчанию и заставить его работать так, как мы изначально ожидали? Чтобы ответить на этот вопрос, мы должны еще раз взглянуть на Документы Ruby для метода Hash::new
.
… Если блок указан, он будет вызываться с хеш-объектом и ключом и должен вернуть значение по умолчанию. Блок обязан сохранить значение в хеше, если это необходимо.
Хорошо, мы можем передать блок в метод Hash::new
. Это означает, что мы можем изменить наш код, чтобы он выглядел так:
my_hash = Hash.new { |hash, key| hash[key] = [] } my_hash[0] << 'This works!' my_hash[1] << 'This works too!' p my_hash #=> {0=>["This works!"], 1=>["This works too!"]} p my_hash[0].object_id #=> 220 p my_hash[1].object_id #=> 240
Так что же здесь происходит? В строке 1 мы снова вызываем метод Hash::new
. Но на этот раз мы передаем блок вместо объекта массива. Как указано в документации, когда мы пытаемся получить доступ к несуществующему ключу, будет вызываться этот блок. Хэш-объект и ключ, к которому мы пытаемся получить доступ, будут переданы в блок в качестве аргументов. Внутри блока мы свяжем новый уникальный объект массива с объектом, присвоенным key
. Возвращаемое значение оператора Hash#[]=
- это значение, используемое для присваивания, поэтому наш блок также вернет то же значение. Это выполняет обязательство по возврату значения по умолчанию из блока, как указано в описании метода Hash::new
.
Возможно, есть более простое решение. Нам не нужно передавать блок в метод Hash::new
, если мы просто не пытаемся изменить значение по умолчанию.
my_hash = Hash.new([]) my_hash[:does_not_exist] += ['Uh oh.'] p my_hash #=> {:does_not_exist=>["Uh oh."]
Обратите особое внимание на операторы, используемые в строке 2. Вместо изменения массива с помощью оператора <<
мы используем оператор +=
для создания строки new, а затем назначаем ее ключу :does_not_exist
. Наше значение по умолчанию остается в покое, а наш хеш правильно изменен.
Я надеюсь, что вы нашли это полезным или, по крайней мере, в некоторой степени интересным. Я столкнулся с этой проблемой и был очень смущен тем, что происходит. Выяснение того, почему код не выполняет то, что я ожидал, было отличной возможностью для обучения, а рассуждения на его основе были отличной возможностью подробно описать, что именно делает написанный мной код. Я хотел бы поблагодарить Эндрю Маршалла за очень полезное объяснение этой темы, опубликованное на Stack Overflow несколько лет назад.