Ruby Hash.new 的陷阱

excosy · 2016年03月08日 · 最后由 excosy 回复于 2016年04月21日 · 2667 次阅读

试问以下代码执行后,b 和 c 的值分别是什么?

a = [1,2,3]
b = Hash.new []
c = Hash.new 0
a.each { |x| b[x.to_s] << x }
a.each { |x| c[x.to_s] += x }

你是否会觉得是这样:

b = {"1"=>[1], "2"=>[2], "3"=>[3]}
c = {"1"=>1, "2"=>2, "3"=>3}

然而实际结果却是:

b = {}
c = {"1"=>1, "2"=>2, "3"=>3}

为什么 b 还是空的?刚 push 的值哪去了?

刚 push 的值在这:

b['0']  #=> [1, 2, 3]
b['1']  #=> [1, 2, 3]

b 的默认值被修改了,换句话说,那些 push 操作只是修改了默认值而已,并没有给 b 增加新元素。

那么 c 为什么又被修改了呢?

对此我的解释是 Hash 的默认值是放在一个不属于 b 和 c 的空间里的,类似于 js 里的 prototype,每次访问不存在的 key 就返回 prototype 里的值。push 操作是直接对原数据进行更改不生成 copy,所以 b 的空间里仍然没有值,反而是默认值被修改了。而数值是简单类型,运算时会产生 copy,这个 copy 就放在 c 的空间里,所以 c 有值,并且 c 的默认值没有被修改。

String 类型生不生成 copy 呢?这取决于你用什么运算符。String 同时提供了<<和+=运算符,前者不提供 copy,后者提供。

如果我想让数组生成 copy 怎么办呢?Hash.new 也有方案支持需要 copy 的数组——block。

若要 b 不为空,第一段代码第二行可改成

b = Hash.new { |hash, key| hash[key] = [] }

这样 b 就能预期地按简单类型——不修改默认值,并且非空那样运作了。 官方文档:http://ruby-doc.org/core-2.3.0/Hash.html#method-c-new

正如 jiazhen 所说,类似的问题还有 Array.new

另外你这里 b 里没值的原因是你写法有问题,与 Hash 默认值共享同一个实例关系不大,你根本就没有为其添加 key,如果你想验证默认的问题,下面的或许更妥:

a.each { |x| b[x.to_s] <<= x }

http://www.rubytapas.com上面有这个问题的视频讲解。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号