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