Странное, неожиданное поведение (исчезновение / изменение значений) при использовании значения Hash по умолчанию, например Hash.new ([])


107

Рассмотрим этот код:

h = Hash.new(0)  # New hash pairs will by default have 0 as values
h[1] += 1  #=> {1=>1}
h[2] += 2  #=> {2=>2}

Все в порядке, но:

h = Hash.new([])  # Empty array as default value
h[1] <<= 1  #=> {1=>[1]}                  ← Ok
h[2] <<= 2  #=> {1=>[1,2], 2=>[1,2]}      ← Why did `1` change?
h[3] << 3   #=> {1=>[1,2,3], 2=>[1,2,3]}  ← Where is `3`?

На этом этапе я ожидаю, что хеш будет:

{1=>[1], 2=>[2], 3=>[3]}

но это далеко не так. Что происходит и как добиться ожидаемого поведения?

Ответы:


164

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

TL; DR : используйте, Hash.new { |h, k| h[k] = [] }если хотите наиболее идиоматическое решение, и вам все равно, почему.


Что не работает

Почему Hash.new([])не работает

Давайте более подробно рассмотрим, почему Hash.new([])не работает:

h = Hash.new([])
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["a", "b"]
h[1]         #=> ["a", "b"]

h[0].object_id == h[1].object_id  #=> true
h  #=> {}

Мы можем видеть, что наш объект по умолчанию повторно используется и видоизменяется (это потому, что он передается как единственное и неповторимое значение по умолчанию, хэш не имеет возможности получить новое, новое значение по умолчанию), но почему нет ключей или значений в массиве, несмотря на то, что h[1]все еще дает нам значение? Вот подсказка:

h[42]  #=> ["a", "b"]

Массив, возвращаемый каждым []вызовом, является просто значением по умолчанию, которое мы все это время изменяли, поэтому теперь он содержит наши новые значения. Поскольку <<не присваивается хешу (в Ruby никогда не может быть присваивания без =подарка † ), мы никогда ничего не помещали в наш фактический хэш. Вместо этого мы должны использовать <<=( <<как +=есть +):

h[2] <<= 'c'  #=> ["a", "b", "c"]
h             #=> {2=>["a", "b", "c"]}

Это то же самое, что:

h[2] = (h[2] << 'c')

Почему Hash.new { [] }не работает

Использование Hash.new { [] }решает проблему повторного использования и изменения исходного значения по умолчанию (поскольку данный блок вызывается каждый раз, возвращая новый массив), но не проблему присваивания:

h = Hash.new { [] }
h[0] << 'a'   #=> ["a"]
h[1] <<= 'b'  #=> ["b"]
h             #=> {1=>["b"]}

Что работает

Путь задания

Если мы будем помнить всегда использовать <<=, то Hash.new { [] } это жизнеспособное решение, но это немного странно и не идиоматические (я никогда не видел <<=использоваться в дикой природе). Он также подвержен незначительным ошибкам, если <<используется непреднамеренно.

Изменчивый путь

Документация дляHash.new государств (курсив мой собственный):

Если блок указан, он будет вызываться с хеш-объектом и ключом и должен вернуть значение по умолчанию. Ответственность за сохранение значения в хэше лежит на блоке, если это необходимо .

Поэтому мы должны сохранить значение по умолчанию в хэше внутри блока, если мы хотим использовать <<вместо <<=:

h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["b"]
h            #=> {0=>["a"], 1=>["b"]}

Это эффективно перемещает назначение из наших индивидуальных вызовов (которые будут использоваться <<=) в переданный блок Hash.new, снимая бремя неожиданного поведения при использовании <<.

Обратите внимание, что есть одно функциональное отличие между этим методом и другими: этот способ присваивает значение по умолчанию при чтении (поскольку присвоение всегда происходит внутри блока). Например:

h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1  #=> {:x=>[]}

h2 = Hash.new { [] }
h2[:x]
h2  #=> {}

Неизменный путь

Вам может быть интересно, почему Hash.new([])не работает, а Hash.new(0)работает нормально. Ключ в том, что числовые значения в Ruby неизменяемы, поэтому мы, естественно, никогда не будем изменять их на месте. Если бы мы относились к нашему значению по умолчанию как к неизменяемому, мы Hash.new([])тоже могли бы использовать :

h = Hash.new([].freeze)
h[0] += ['a']  #=> ["a"]
h[1] += ['b']  #=> ["b"]
h[2]           #=> []
h              #=> {0=>["a"], 1=>["b"]}

Однако обратите внимание на это ([].freeze + [].freeze).frozen? == false. Итак, если вы хотите гарантировать, что неизменность сохраняется повсюду, вы должны позаботиться о повторном замораживании нового объекта.


Вывод

Из всех способов я лично предпочитаю «неизменный путь» - неизменность обычно делает рассуждения о вещах намного проще. В конце концов, это единственный метод, у которого нет возможности скрытого или незаметного неожиданного поведения. Однако наиболее распространенный и идиоматический способ - это «изменчивый путь».

И наконец, такое поведение значений хэша по умолчанию отмечено в Ruby Koans .


† Это не совсем так, такие методы, как instance_variable_setобход этого, но они должны существовать для метапрограммирования, поскольку l-значение in =не может быть динамическим.


1
Следует отметить, что использование «изменяемого способа» также приводит к тому, что каждый поиск по хешу сохраняет пару значений ключа (поскольку в блоке происходит присвоение), что не всегда может быть желательным.
— johncip

@johncip Не каждый поиск, только первый для каждого ключа. Но я понимаю, что вы имеете в виду, я добавлю это к ответу позже; Спасибо!.
— Эндрю Маршалл

Упс, неряшливо. Вы правы, конечно, это первый поиск неизвестного ключа. Я почти чувствую, что у { [] }with <<=меньше всего сюрпризов, если бы не тот факт, что случайное забывание =могло бы привести к очень запутанному сеансу отладки.
— johncip

довольно четкие объяснения различий при инициализации хэша значениями по умолчанию
— cisolarix

23

Вы указываете, что значение по умолчанию для хэша является ссылкой на этот конкретный (изначально пустой) массив.

Я думаю, ты хочешь:

h = Hash.new { |hash, key| hash[key] = []; }
h[1]<<=1 
h[2]<<=2 

Это устанавливает значение по умолчанию для каждого ключа в новый массив.


Как я могу использовать отдельные экземпляры массива для каждого нового хеша?
— Валентин Васильев

5
Эта блочная версия дает вам новые Arrayэкземпляры при каждом вызове. А именно: h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570. Также: если вы используете версию блока, которая устанавливает значение ( {|hash,key| hash[key] = []}), а не ту, которая просто генерирует значение ( { [] }), тогда вам нужно только <<, а не <<=при добавлении элементов.
— Джеймс А. Розен,

3

Оператор, +=примененный к этим хешам, работает должным образом.

[1] pry(main)> foo = Hash.new( [] )
=> {}
[2] pry(main)> foo[1]+=[1]
=> [1]
[3] pry(main)> foo[2]+=[2]
=> [2]
[4] pry(main)> foo
=> {1=>[1], 2=>[2]}
[5] pry(main)> bar = Hash.new { [] }
=> {}
[6] pry(main)> bar[1]+=[1]
=> [1]
[7] pry(main)> bar[2]+=[2]
=> [2]
[8] pry(main)> bar
=> {1=>[1], 2=>[2]}

Это может быть потому, что foo[bar]+=bazэто синтаксический сахар, foo[bar]=foo[bar]+bazкогда foo[bar]при оценке справа =он возвращает объект значения по умолчанию, и +оператор не изменит его. Левая часть - синтаксический сахар для []=метода, который не изменяет значение по умолчанию .

Обратите внимание , что это не относится к , foo[bar]<<=bazкак это будет эквивалентно foo[bar]=foo[bar]<<bazи << будет изменить значение по умолчанию .

Кроме того, я не нашел разницы между Hash.new{[]}и Hash.new{|hash, key| hash[key]=[];}. Хотя бы на рубине 2.1.2.


Хорошее объяснение. Похоже, что на ruby ​​2.1.1 Hash.new{[]}то же самое, что и Hash.new([])у меня, с отсутствием ожидаемого <<поведения (хотя, конечно, Hash.new{|hash, key| hash[key]=[];}работает). Странные мелочи, ломающие все: /
— butterywombat

1

Когда пишешь,

h = Hash.new([])

вы передаете ссылку на массив по умолчанию всем элементам в хеше. из-за этого все элементы в хэше относятся к одному и тому же массиву.

если вы хотите, чтобы каждый элемент в хэше ссылался на отдельный массив, вы должны использовать

h = Hash.new{[]} 

для более подробной информации о том, как это работает в Ruby, просмотрите это: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new


Это неправильно, Hash.new { [] }это не работает. Подробности смотрите в моем ответе . Это также уже решение, предложенное в другом ответе.
— Эндрю Маршалл
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.