Ruby method Array # << не обновляет массив в хеше

Вдохновленный Как я могу маршировать хэш с массивами? Интересно, какая причина, по которой Array#<< не будет работать должным образом в следующем коде:

h = Hash.new{Array.new}
#=> {}
h[0]
#=> []
h[0] << 'a'
#=> ["a"]
h[0]
#=> [] # why?!
h[0] += ['a']
#=> ["a"]
h[0]
#=> ["a"] # as expected

Это связано с тем, что << изменяет массив на месте, а Array#+ создает новый экземпляр?

Ответ 1

Если вы создаете Hash с использованием формы блока Hash.new, блок будет выполняться каждый раз при попытке получить доступ к элементу, который на самом деле не существует. Итак, давайте просто посмотрим, что получится:

h = Hash.new { [] }
h[0] << 'a'

Первое, что оценивается здесь, - это выражение

h[0]

Что происходит, когда он оценивается? Ну, блок запускается:

[]

Это не очень интересно: блок просто создает пустой массив и возвращает его. Он ничего не делает. В частности, он никак не меняет h: h по-прежнему пуст.

Затем сообщение << с одним аргументом 'a' отправляется в результат h[0], который является результатом блока, который является просто пустым массивом:

[] << 'a'

Что это делает? Он добавляет элемент 'a' в пустой массив, но поскольку массив фактически не привязан к какой-либо переменной, он немедленно собирает мусор и уходит.

Теперь, если вы снова оцените h[0]:

h[0] # => []

h по-прежнему пуст, так как ничто никогда не было назначено ему, поэтому ключ 0 по-прежнему не существует, что означает, что блок снова запускается, а это означает, что он снова возвращает пустой массив (но обратите внимание, что теперь это новый, новый пустой массив).

h[0] += ['a']

Что здесь происходит? Во-первых, оператор присваивания получает desugared до

h[0] = h[0] + ['a']

Теперь оценивается h[0] с правой стороны. И что он возвращается? Мы уже рассмотрели это: h[0] не существует, поэтому блок запускается, блок возвращает пустой массив. Опять же, это совершенно новый, третий пустой массив. Этот пустой массив отправляет сообщение + с аргументом ['a'], что заставляет его возвращать еще один новый массив, который является массивом ['a']. Затем этот массив присваивается h[0].

Наконец, на этом этапе:

h[0] # => ['a']

Теперь вы, наконец, положили что-то в h[0], поэтому, очевидно, вы получите то, что вы вложили.

Итак, чтобы ответить на вопрос, который у вас, вероятно, был, почему бы вам не разобраться, что вы вложили? Во-первых, вы ничего не вкладывали!

Если вы действительно хотите назначить хэш внутри блока, вам нужно назначить хэш внутри блока:

h = Hash.new {|this_hash, nonexistent_key| this_hash[nonexistent_key] = [] }
h[0] << 'a'
h[0] # => ['a']

На самом деле довольно легко увидеть, что происходит в вашем примере кода, если вы посмотрите на идентичность объектов. Затем вы можете видеть, что каждый раз, когда вы вызываете h[0], вы получаете другой массив.

Ответ 2

Проблема в вашем коде заключается в том, что h[0] << 'a' создает новый массив и выдает его при индексировании с помощью h[0], но не сохраняет измененный массив где-нибудь после << 'a', потому что нет назначения.

Между тем h[0] += ['a'] работает, потому что он эквивалентен h[0] = h[0] + ['a']. Это имеет значение ([]=).

Первый случай может показаться запутанным, но он полезен, когда вы просто хотите получить какой-то неизменный элемент по умолчанию из Hash, когда ключ не найден. В противном случае вы могли бы заполнить Hash большим количеством неиспользуемых значений, просто проиндексировав его.

Ответ 3

h = Hash.new{ |a,b| a[b] = Array.new }
h[0] << "hello world"
#=> ["hello world"]
h[0]
#=> ["hello world"]