Ответ 1
Метод #rehash будет пересчитывать хэш, поэтому после изменения ключа выполните:
b.rehash
У меня есть хэш, который использует массив в качестве ключа. Когда я изменяю массив, хэш больше не может получить соответствующий ключ и значение:
1.9.3p194 :016 > a = [1, 2]
=> [1, 2]
1.9.3p194 :017 > b = { a => 1 }
=> {[1, 2]=>1}
1.9.3p194 :018 > b[a]
=> 1
1.9.3p194 :019 > a.delete_at(1)
=> 2
1.9.3p194 :020 > a
=> [1]
1.9.3p194 :021 > b
=> {[1]=>1}
1.9.3p194 :022 > b[a]
=> nil
1.9.3p194 :023 > b.keys.include? a
=> true
Что я делаю неправильно?
Обновление: ОК. Использовать a.clone - это абсолютно один из способов решения этой проблемы. Что делать, если я хочу изменить "a" , но все равно использовать "a" для получения соответствующего значения (поскольку "a" все еще является одним из ключей)?
Метод #rehash будет пересчитывать хэш, поэтому после изменения ключа выполните:
b.rehash
TL; DR: рассмотрим Hash#compare_by_indentity
По умолчанию массивы .hash
и .eql?
по значению, поэтому изменение значения смущает ruby. Рассмотрите этот вариант вашего примера:
pry(main)> a = [1, 2]
pry(main)> a1 = [1]
pry(main)> a.hash
=> 4266217476190334055
pry(main)> a1.hash
=> -2618378812721208248
pry(main)> h = {a => '12', a1 => '1'}
=> {[1, 2]=>"12", [1]=>"1"}
pry(main)> h[a]
=> "12"
pry(main)> a.delete_at(1)
pry(main)> a
=> [1]
pry(main)> a == a1
=> true
pry(main)> a.hash
=> -2618378812721208248
pry(main)> h[a]
=> "1"
Видишь, что там произошло?
Как вы обнаружили, он не соответствует ключу a
, поскольку значение .hash
, под которым он хранится, устарело [Кстати, вы даже не можете на это полагаться! Мутация может привести к тому же хешу (редко) или другому хешу, который попадает в одно и то же ведро (не так уж и редко).]
Но вместо того, чтобы вернуть nil
, он соответствовал клавише a1
.
Видите, h[a]
совершенно не заботится о личности a
против a1
(предателя!). Он сравнил текущее значение, которое вы предоставляете - [1]
со значением a1
, являющимся [1]
, и нашел соответствие.
Вот почему с помощью .rehash
просто пластырь. Он пересчитает значения .hash
для всех ключей и переместит их в правильные сегменты, но он подвержен ошибкам и может также вызвать проблемы:
pry(main)> h.rehash
=> {[1]=>"1"}
pry(main)> h
=> {[1]=>"1"}
О, о Две записи свернулись в одну, так как теперь они имеют одинаковое значение (и какой выигрыш трудно предсказать).
Один разумный подход - это поиск по значению, который требует, чтобы значение никогда не менялось. .freeze
ваши ключи. Или используйте .clone
/.dup
при построении хеша и не стесняйтесь изменять исходные массивы - но примите, что h[a]
будет искать текущее значение a
в сравнении со значениями, сохраненными во время сборки.
Другой, который, по-видимому, вам нужен, решает, что вы заботитесь об идентичности - поиск по a
должен найти a
независимо от его текущего значения, и не должно иметь значения, имели ли многие ключи одинаковое значение.
Как?
Object
хэши по идентичности. (Массивы этого не делают, потому что типы, которые .==
по значению имеют тенденцию также переопределять .hash
и .eql?
, чтобы быть по значению.) Поэтому один из вариантов: не используйте массивы в качестве ключей, используйте некоторый пользовательский класс (который может держать массив внутри).
Но что, если вы хотите, чтобы он вел себя как хэш массивов? Вы можете создать подкласс Hash или Array, но это очень много, чтобы все работало согласованно. К счастью, у Ruby есть встроенный способ: h.compare_by_identity
переключает хеш на работу по идентификатору (без возможности отменить, AFAICT). Если вы сделаете это до того, как вставите что-либо, у вас даже могут быть разные ключи с одинаковыми значениями, без путаницы:
[39] pry(main)> x = [1]
=> [1]
[40] pry(main)> y = [1]
=> [1]
[41] pry(main)> h = Hash.new.compare_by_identity
=> {}
[42] pry(main)> h[x] = 'x'
=> "x"
[44] pry(main)> h[y] = 'y'
=> "y"
[45] pry(main)> h
=> {[1]=>"x", [1]=>"y"}
[46] pry(main)> x.push(7)
=> [1, 7]
[47] pry(main)> y.push(7)
=> [1, 7]
[48] pry(main)> h
=> {[1, 7]=>"x", [1, 7]=>"y"}
[49] pry(main)> h[x]
=> "x"
[50] pry(main)> h[y]
=> "y"
Помните, что такие хэши нелогичны, если вы пытаетесь их там разместить, например, strings, потому что мы действительно привыкли к хэшированию строк по значению.
Хэши используют хэш-коды своих ключевых объектов (a.hash
) для их группировки. Хэш-коды часто зависят от состояния объекта; в этом случае хеш-код a
изменяется, когда элемент был удален из массива. Так как ключ уже вставлен в хэш, a
хранится под его исходным хеш-кодом.
Это означает, что вы не можете получить значение для a
в b
, даже если оно выглядит хорошо, когда вы печатаете хэш.
Вы должны использовать a.clone
как ключ
irb --> a = [1, 2]
==> [1, 2]
irb --> b = { a.clone => 1 }
==> {[1, 2]=>1}
irb --> b[a]
==> 1
irb --> a.delete_at(1)
==> 2
irb --> a
==> [1]
irb --> b
==> {[1, 2]=>1} # STILL UNCHANGED
irb --> b[a]
==> nil # Trivial, since a has changed
irb --> b.keys.include? a
==> false # Trivial, since a has changed
Использование a.clone
гарантирует, что ключ не изменится, даже если мы изменим a
позже.
Как вы уже сказали, проблема в том, что хэш-ключ - это тот же самый объект, который вы позже модифицируете, что означает, что ключ изменяется во время выполнения программы.
Чтобы избежать этого, сделайте копию массива для использования в качестве хеш-ключа:
a = [1, 2]
b = { a.clone => 1 }
Теперь вы можете продолжить работу с a
и оставить свои хеш-ключи целыми.