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

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

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]}

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

Ответы [ 4 ]

158 голосов
/ 07 марта 2015

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

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 в = не может быть динамическим.

23 голосов
/ 23 апреля 2010

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

Я думаю, что вы хотите:

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

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

3 голосов
/ 25 сентября 2014

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

[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]=[];}. По крайней мере, на ruby ​​2.1.2.

1 голос
/ 03 мая 2016

Когда вы пишете,

h = Hash.new([])

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

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

h = Hash.new{[]} 

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...