17

我有一个使用数组作为键的哈希。当我更改数组时,哈希无法再获取对应的键和值:

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”仍然是键之一)怎么办?

4

5 回答 5

18

#rehash方法将重新计算哈希,因此在键更改后:

b.rehash
于 2012-08-29T13:37:37.097 回答
8

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上匹配。 看,根本不在乎vs的身份(叛徒!)。它将您提供的当前与存在的值进行比较并找到匹配项。a1
h[a]aa1[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 ,则此类散列是违反直觉的,因为我们确实习惯于按值散列的字符串。

于 2016-04-24T10:00:44.967 回答
2

散列使​​用其关键对象的散列码 ( a.hash) 对它们进行分组。哈希码通常取决于对象的状态;a在这种情况下,当从数组中删除一个元素时,哈希码会发生变化。由于密钥已经插入到散列中,a因此被归档在其原始散列码下。

这意味着您无法检索ain的值b,即使在打印散列时它看起来没问题。

于 2012-08-29T13:04:14.673 回答
1

您应该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稍后更改密钥也不会更改。

于 2012-08-29T11:44:02.917 回答
1

正如您已经说过的,问题在于哈希键与您稍后修改的对象完全相同,这意味着键在程序执行期间会发生变化。

为避免这种情况,请复制数组以用作哈希键:

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

现在您可以继续使用a并保持哈希键不变。

于 2012-08-29T11:52:22.277 回答