Ruby ruby 中的浅拷贝

loveky · 2012年03月08日 · 最后由 loveky 回复于 2012年03月08日 · 5245 次阅读

正在看《The Ruby Programming Language》,书中说到 clone 是浅拷贝,于是做了如下测试

irb(main):007:0> class Klass
irb(main):008:1> attr_accessor :str
irb(main):009:1> end
=> nil
irb(main):010:0> s1 = Klass.new
=> #<Klass:0x7f301a290d50>
irb(main):011:0> s1.str = "Hello"
=> "Hello"
irb(main):012:0> s2 = s1.clone
=> #<Klass:0x7f301a281e90 @str="Hello">
irb(main):013:0> s1
=> #<Klass:0x7f301a290d50 @str="Hello">
irb(main):014:0> s2
=> #<Klass:0x7f301a281e90 @str="Hello">
irb(main):015:0> s2.str[1,4] = "i"
=> "i"
irb(main):016:0> s1
=> #<Klass:0x7f301a290d50 @str="Hi">
irb(main):017:0> s2
=> #<Klass:0x7f301a281e90 @str="Hi">

以上 s1 的内容随着 s2 的改变而改变,说明 clone 是一个浅拷贝

irb(main):018:0> a = Array.new
=> []
irb(main):019:0> a.push(1).push(2).push(3)
=> [1, 2, 3]
irb(main):020:0> a
=> [1, 2, 3]
irb(main):021:0> b = a.clone
=> [1, 2, 3]
irb(main):022:0> a
=> [1, 2, 3]
irb(main):023:0> b
=> [1, 2, 3]
irb(main):024:0> b.push(4)
=> [1, 2, 3, 4]
irb(main):025:0> a
=> [1, 2, 3]

问题:为什么这里 a,b 两个数组的内容不一样?

不同的类,实现的 clone 方法不一样。Array 对第一层对象做深度拷贝,但是嵌套的对象还是 shadow copy。试试这个:

ruby-1.9.2-p0 > a = Array.new
[]
ruby-1.9.2-p0 > a << ['first']
[
    [0] [
        [0] "first"
    ]
]
ruby-1.9.2-p0 > b = a.clone
[
    [0] [
        [0] "first"
    ]
]
ruby-1.9.2-p0 > b[0] << 'second'
[
    [0] "first",
    [1] "second"
]
ruby-1.9.2-p0 > a
[
    [0] [
        [0] "first",
        [1] "second"
    ]
]

#1 楼 @daqing 非常感谢解答

我的理解,浅拷只拷到第一层内置变量。 class Klass attr_accessor :str end 上面这个 str 是方法,不是变量。

变量是这样的,比如 RString 中的 ptr

 struct RString {
653     struct RBasic basic;
654     union {
655     struct {
656         long len;
657         char *ptr;
658         union {
659         long capa;
660         VALUE shared;
661         } aux;
662     } heap;
663     char ary[RSTRING_EMBED_LEN_MAX + 1];
664     } as;
665 };

#1 楼 @daqing @loveky 其实不是这样的。 在 Ruby 里,我们所说的变量等实际都是对 对像的引用。浅拷贝只是拷贝引用而已,不是拷贝引用所指的对象。

m = ['a','b']                      # ["a", "b"] 
n = m.clone                        # ["a", "b"]

m.object_id == n.object_id         # false
m[0].object_id == n[0].object_id   # true
m[1].object_id == n[1].object_id   # true


代码中 n 是由 m 克隆来的。指向一个新对象,所以 id 不同。 但是,m 和 n 中的元素都是指向相同的对象,对象本身并没有进行拷贝。所以 id 是相同的。 当然,这和 Array 的第 1 层或是第几层也没关系。

想实现深 copy,可以自行实现 initialize_copy 来达到深 copy 的目的。

#3 楼 @hhuai 那个attr_accessor :str会创建实例变量@str

#5 楼 @daqing @str也只是一个引用而已。 :>

#5 楼 @daqing 你在东营吗?我家在东营住,回去时有时间聚一下哈 :>

#7 楼 @skandhas 我在河口,没想到还能遇到东营的。。

#8 楼 @daqing 我家在东营~ 人现在沈阳。呵呵 :> 此楼已歪:)

#4 楼 @skandhas +1 initialize_copy 是正解 还可以用序列化的方式:http://www.iteye.com/topic/407957

#10 楼 @hooopo 好文章~ 总结的很全面。以前就看过你的这篇文章,受教了 :>

#4 楼 @skandhas 既然这样,为何修改其中一个,另一个不会跟着修改呢?

#12 楼 @loveky 你是指你的第二个关于数组的例子吗? 大体流程:

  1. b = a.clone 会产生一个新的 Array 对象 b(其实是引用) a 中包含的原有的对象引用也被拷贝到 b。注意是引用,不是对象。如下例中 m 的‘a’这个对象引用。
  2. b.push(4) 是对 b 所引用的对象进行操作,这个对象和 a 所引用的对象不一样,当然不会改变 a。

为了你不迷糊,缩小一下叙述范围,如果一个对象内部含有对其他对象的引用,在浅拷贝时,先创建一个新对象,然后再拷贝内部包含的其他对象 (比如说是实例变量) 的引用 (这些是引用,而不是对象)。

m=['a']
n=m.clone    #  克隆产生一个新对象。

n[0] << 'b'    # n[0]和m[0]都指向都一个对象,对其修改,m[0]和n[0]都会改变

p n  # ["ab"]    
p m  # ["ab"]

n << 'c'    #对n 所引用的对象进行操作
p m          # ["ab"]   m没有变化  
p n          # ["ab","c"]   n 增加了一个元素





建议你看看@hooopo 的帖子。其实,关于对象引用的问题,给你画个图你就能明白了。写字反而容易让人迷糊。

@loveky 应该了解 c 语言吧。

我觉得也可以这样理解: 你的 a,b 两个数组例子类似

int a[4] = { 1, 2, 3, 4 };

// clone
int *b    = (int *)malloc(sizeof(a);
memcpy(b, a, sizeof(a));

如果写 b[1] = 8; 则 a[1] 仍然为 2.

而如果写成如下的代码

int *a[4];
int e1 = 1, e2 = 2, e3 = 3, e4 = 5;

a[0] = &e1;
a[1] = &e2;
a[2] = &e3;
a[3] = &e4;

// clone
int *b[4];
memcpy( b, a, sizeof(a) );

如果写 (b[1]) = 8; 则(a[1]) 会变成 8

语言不一样,道理类似。

应该为 *(b[1]) = 8; 则*(a[1]) 会变成 8

忘了 markdown,会把 * 处理了,不好意思

#13 楼 @skandhas 非常感谢你的耐心解释,这么解释就容易明白了

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