新手问题 已经被 Ruby 的引用方式搞晕

wanzysky · 2016年03月13日 · 最后由 zhangbin-github 回复于 2016年11月16日 · 2482 次阅读
#!/usr/local/bin/ruby
class A
  attr_accessor :a_fixnum, :a_string, :an_array

  def initialize fixnum, string, array
    @a_fixnum = fixnum
    @a_string = string
    @an_array = array
  end
end

a = A.new 0, '', []
b = a.clone
p "b.__id__ == a.__id__ #{b.__id__ == a.__id__}"
p "b.a_fixnum.__id__ == a.a_fixnum.__id__ #{b.a_fixnum.__id__ == a.a_fixnum.__id__}"
p "b.a_string.__id__ == a.a_string.__id__ #{b.a_string.__id__ == a.a_string.__id__}"

a.a_fixnum += 1
a.a_string << '2'
a.an_array << 3

p a
p b

结果是:

  ~ ./bug_of_ruby.rb
"b.__id__ == a.__id__ false"
"b.a_fixnum.__id__ == a.a_fixnum.__id__ true"
"b.a_string.__id__ == a.a_string.__id__ true"
#<A:0x007ff583079150 @a_fixnum=1, @a_string="2", @an_array=[3]>
#<A:0x007ff583079128 @a_fixnum=0, @a_string="2", @an_array=[3]>
  ~

可是我们知道,直接 clone 一个 array 是不会有这样的表现的

[2] pry(main)> a = [1,2,3]
=> [1, 2, 3]
[3] pry(main)> b = a.clone
=> [1, 2, 3]
[4] pry(main)> a.pop
=> 3
[5] pry(main)> b
=> [1, 2, 3]

个人认为这种诡异的行为给编程带来很多隐患,有什么优雅的变法避免吗

  1. It's not a bug, it's a feature.
  2. clone/dup 默认都是拷贝引用
  3. 直接 clone 一个 Array 表现是一样的,你的例子对比不当。对于数组,a 与 b 的 __id__ 不同,但是 a[0] 与 b[0] 的 __id__ 相同;正如 a = A.new 与 b = a.clone 的 __id__ 不同,但是 a.a_fixnum 与 b.a_fixnum 的 __id__ 相同
  4. a.a_fixnum += 1 其实是 a = a.a_fixnum + 1,这样之后,a.a_fixnum 引用的对象就变了,那么 __id__ 自然就变了,两边的对象数据就不一样了。a.a_string << '2' 其实是 a.a_string.push('2') ,这是原地修改操作,没有创建新的对象,所以 __id__ 不会变化,但两边的对象数据都会变化。
  5. 如果要创建全新的对象,可以通过序列化手段。或者去重写 initialize_copy 之类的方法。

Array 和 Object 在内存中的数据结构是不一样的,所以 clone 出来的结果也不一样。

对 array 执行 clone 的时候,其实是在内存中创建了一个新的 array 并且复制了所有的原数组元素的引用,这个时候你对新的 array 执行 pop 操作,只是把一个引用从新的 array 中移除,对原数组没有影响,所以这是 feature,不是 bug。

#1 楼 @watraludru 回答的更仔细。

#1 楼 @watraludru 茅塞顿开,太感谢您了

dup 和 clone 都是浅 copy,只 copy 数组本身的引用,不 copy 数组内元素的引用. 要想深 copy,使用 Marshal.load 和 dump,但是 marshal 不支持闭包和单例

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