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