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