Дивна, несподівана поведінка (зникнення / зміна значень) при використанні значення 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. Отже, якщо ви хочете забезпечити збереження незмінності протягом усього часу, тоді вам слід подбати про повторне заморожування нового об’єкта.


Висновок

З усіх способів я особисто віддаю перевагу "незмінному способу" - непорушність, як правило, робить міркування про речі набагато простішими. Зрештою, це єдиний метод, який не має можливості прихованої або тонкої несподіваної поведінки. Однак найпоширенішим і ідіоматичним способом є "спосіб, що змінюється".

В кінцевому підсумку така поведінка значень за замовчуванням Hash відзначена в Ruby Koans .


Це не зовсім вірно, такі методи як instance_variable_setобхід цього, але вони повинні існувати для метапрограмування, оскільки значення l =не може бути динамічним.


1
Слід зазначити, що використання "мутаційного способу" також призводить до того, що кожен пошук пошуку хешу зберігає пару ключових значень (оскільки в блоці відбувається призначення), що може не завжди бути бажаним.
johncip

@johncip Не кожен пошук, лише перший для кожної клавіші. Але я бачу, що ви маєте на увазі, я додам це до відповіді пізніше; Дякую!.
Ендрю Маршалл

Упс, будучи неохайним. Ви праві, звичайно, це перший пошук невідомого ключа. Я майже відчуваю , як { [] }з <<=має найменшу кількість сюрпризів, якщо б не той факт , що випадково забуваючи =може привести до дуже бентежити сеансу налагодження.
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.


Приємне пояснення. Здається, на рубіні 2.1.1 Hash.new{[]}те саме, що і Hash.new([])для мене з відсутністю очікуваної <<поведінки (хоча, звичайно, Hash.new{|hash, key| hash[key]=[];}працює). Дивні дрібниці, що порушують усі речі: /
butterywombat

1

Коли ви пишете,

h = Hash.new([])

Ви передаєте посилання за замовчуванням масиву на всі елементи в хеші. через те всі елементи в хеші відноситься до одного масиву.

якщо ви хочете, щоб кожен елемент у хеші посилався на окремий масив, вам слід скористатися

h = Hash.new{[]} 

Детальніше про те, як це працює в рубіні, перегляньте це: 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.