Хэш-массив в качестве ключа в рубине

У меня есть хэш, который использует массив в качестве ключа. Когда я изменяю массив, хэш больше не может получить соответствующий ключ и значение:

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" все еще является одним из ключей)?

Ответы

Ответ 1

Метод #rehash будет пересчитывать хэш, поэтому после изменения ключа выполните:

b.rehash

Ответ 2

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

Ответ 3

Хэши используют хэш-коды своих ключевых объектов (a.hash) для их группировки. Хэш-коды часто зависят от состояния объекта; в этом случае хеш-код a изменяется, когда элемент был удален из массива. Так как ключ уже вставлен в хэш, a хранится под его исходным хеш-кодом.

Это означает, что вы не можете получить значение для a в b, даже если оно выглядит хорошо, когда вы печатаете хэш.

Ответ 4

Вы должны использовать 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 позже.

Ответ 5

Как вы уже сказали, проблема в том, что хэш-ключ - это тот же самый объект, который вы позже модифицируете, что означает, что ключ изменяется во время выполнения программы.

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

a = [1, 2]
b = { a.clone => 1 }

Теперь вы можете продолжить работу с a и оставить свои хеш-ключи целыми.