深浅拷贝

道歉

之前我的一篇关于深浅拷贝的文章,里面有诸多错误,主要是混淆了混淆copy、mutableCopy和深浅拷贝,给大家带来了误导,这里我深表歉意。

经过大家的指正和参考前辈的文章:http://www.cocoachina.com/bbs/read.php?tid-323045-page-1.html。

以及在网上搜集了各种资料,我已经删除了原文,在这里做出了更改,但是肯定还是有纰漏之处,欢迎大家不吝赐教。


理解深浅拷贝

先来看看苹果文档的定义:

There are two kinds of object copying: shallow copies and deep copies. The normal copy is a shallow copy that produces a new collection that shares ownership of the objects with the original. Deep copies create new objects from the originals and add those to the new collection.

In the case of these objects, a shallow copy means that a new collection object is created, but the contents of the original collection are not duplicated—only the object references are copied to the new container.
A deep copy duplicates the compound object as well as the contents of all of its contained objects.

再来看看stackoverfolow网友的总结:

Shallow copies duplicate as little as possible. A shallow copy of a collection is a copy of the collection structure, not the elements. With a shallow copy, two collections now share the individual elements.

Deep copies duplicate everything. A deep copy of a collection is two collections with all of the elements in the original collection duplicated.

简单来说:

  • 浅拷贝:只是复制容器本身,不会复制容器内部的元素,浅拷贝后生成的新容器对象和原始容器对象共享内部元素
  • 深拷贝:不仅复制容器本身,容器内部的元素也会复制,深拷贝后生成的新容器对象和原始容器的内部元素是独立的。

理解copy和mutableCopy

  • copy:不可变拷贝,遵循NSCopying协议,需要对应实现copyWithZone方法
  • mutableCopy:可变拷贝,遵循NSMutableCopying协议,需要对应实现mutableCopyWithZone:方法

系统容器类NSArray和NSDictonary都已经实现了上述两个协议。


纠错一、混淆copy、mutableCopy和深浅拷贝

网上流传很多的一张图,我也被误导过

image

上图得出了两个错误结论:

  1. 对于不可变或者可变对象的mutableCopy操作都是深拷贝
  2. 对于可变对象的copy操作是深拷贝

这是错的!!!

正确理解

对于不可变对象和可变对象的mutableCopy或者copy操作都是浅拷贝!!!

如何验证

那么如何证明呢?

我们先来看看深拷贝的定义:

深拷贝:不仅复制容器本身,容器内部的元素也会复制,深拷贝后生成的新容器对象和原始容器的内部元素是独立的。

那么就可以观察改变源容器的内部元素会不会影响新生成容器的内部元素来证明,影响就说明是浅拷贝,不影响就说明是深拷贝。

1. 代码:
    NSMutableArray * dataArray2=[NSMutableArray arrayWithObjects:
                                [NSMutableString stringWithString:@"one"],
                                [NSMutableString stringWithString:@"two"],
                                [NSMutableString stringWithString:@"three"],
                                [NSMutableString stringWithString:@"four"],
                                nil
                                ];

    NSMutableArray * dataArray3;
    NSMutableString * mStr;

    dataArray3=[dataArray2 mutableCopy];

    mStr = dataArray2[0];
    [mStr appendString:@"--ONE"];

    NSLog(@"dataArray3:%@",dataArray3);
    NSLog(@"dataArray2:%@",dataArray2);
2.输出:
2016-07-31 17:40:30.702 test1[2113:169774] dataArray3:(  
    "one--ONE",
    two,
    three,
    four,
        (
        1,
        2,
        3,
        4
    )
)
2016-07-31 17:40:30.703 test1[2113:169774] dataArray2:(  
    "one--ONE",
    two,
    three,
    four,
        (
        1,
        2,
        3,
        4
    )
)
3.结论:

通过代码可以看到改变了原始数组dataArray2的第一个元素,dataArray3的第一个元素也发生了改变,说明这是浅拷贝。 其他两种情况我就不验证了,大家可以自己验证。你会发现对于可变和不可变对象的copy和mutablCopy操作都是浅复制,区别仅仅是生成的新对象是可变还是不可变的,copy生成不可变对象,mutableCopy生成可变对象!!!!

纠错二:浅拷贝是指针复制,深拷贝是内容复制

同样是流传很广的一张图 image

这句话前半句是错的,后半句是对的。

但是前半句非常迷惑人,而且网上还有证明这句话是对的代码。

如下:

    NSArray *array = @[@1];
    NSLog(@"retainCount:%ld", [array retainCount]);

    NSArray *array2 = [array copy];

    NSLog(@"retainCount:%ld", [array2 retainCount]);
    NSLog(@"内存地址:%p---%p", array,array2);

输出:

2016-08-04 20:25:39.830 test1[27081:776720] retainCount:1  
2016-08-04 20:25:39.831 test1[27081:776720] retainCount:2  
2016-08-04 20:25:39.831 test1[27081:776720] 内存地址:0x7fa0131175c0---0x7fa0131175c0

可以看到引用计数确实增加了1,而且浅复制前后的内存地址都是一样的,说明是同一个对象,所以得出结论:copy操作只是一次retain操作,对指针进行了复制。

但是真的是这样吗?

真实的情况:

对不可变对象进行copy操作,实际上是把原始对象的的指针的值赋值给生成的新对象的指针的值,这样两个对象的指针的值(对象存放的内存地址)就是相同的,也就导致copy操作之后引用计数增加。

但是怎么证明,我还没找到办法,望大家赐教。

结论:

  • copy操作返回的必然是一个不可变对象,无论源对象是可变对象还是不可变对象。如果源对象是一个不可变对象,那么它们(源对象和新生成的对象)指向同一个对象同一块内存地址,如果源对象是可变对象,它们指向不同对象不同内存地址。(被横线划掉的地方描述不精准,故划去,如果对这里有疑惑的同学可以看下面的评论回复,我做了详细解释_
  • mutableCopy]返回的必然是一个可变对象,无论源对象是可变对象还是不可变对象,它们(源对象和新生成的对象)仍指向不同地址,是两个对象。
  • copy和mutableCopy都生成新对象

那么既然copy和mutableCopy都无法实现深拷贝,那我们只能自己手动实现了。但是这里要注意深复制还分为单层深拷贝和完全深拷贝,下面具体来看看

单层深拷贝

只会复制容器内部的第一层对象,对于容器内部包含的容器的内部对象不进行复制。

代码:

    NSMutableArray * dataArray1=[NSMutableArray arrayWithObjects:
                                 [NSMutableString stringWithString:@"1"],
                                 [NSMutableString stringWithString:@"2"],
                                 [NSMutableString stringWithString:@"3"],
                                 [NSMutableString stringWithString:@"4"],
                                 nil

                                 ];
    NSMutableArray * dataArray2=[NSMutableArray arrayWithObjects:
                                 [NSMutableString stringWithString:@"one"],
                                 [NSMutableString stringWithString:@"two"],
                                 [NSMutableString stringWithString:@"three"],
                                 [NSMutableString stringWithString:@"four"],
                                 dataArray1,
                                 nil
                                 ];

    NSMutableArray * dataArray3;
    NSMutableString * mStr;

    dataArray3=[[NSMutableArray alloc]initWithArray:dataArray2 copyItems:YES];

    mStr = dataArray2[0];
    [mStr appendString:@"--ONE"];

    NSLog(@"dataArray3:%@",dataArray3);
    NSLog(@"dataArray2:%@",dataArray2);

输出如下:

2016-07-31 17:45:48.472 test1[2151:173221] dataArray3:(  
    one,
    two,
    three,
    four,
        (
        1,
        2,
        3,
        4
    )
)
2016-07-31 17:45:48.472 test1[2151:173221] dataArray2:(  
    "one--ONE",
    two,
    three,
    four,
        (
        1,
        2,
        3,
        4
    )
)

可以看到dataArray3并没有被改变,但是别高兴的太早,我们再来改改。

代码如下:

    NSMutableArray * dataArray1=[NSMutableArray arrayWithObjects:
                                [NSMutableString stringWithString:@"1"],
                                [NSMutableString stringWithString:@"2"],
                                [NSMutableString stringWithString:@"3"],
                                [NSMutableString stringWithString:@"4"],
                                nil

                                ];
    NSMutableArray * dataArray2=[NSMutableArray arrayWithObjects:
                                [NSMutableString stringWithString:@"one"],
                                [NSMutableString stringWithString:@"two"],
                                [NSMutableString stringWithString:@"three"],
                                [NSMutableString stringWithString:@"four"],
                                dataArray1,
                                nil
                                ];

    NSMutableArray * dataArray3;
    NSMutableString * mStr;

    dataArray3=[[NSMutableArray alloc]initWithArray:dataArray2 copyItems:YES];

    NSMutableArray *mArr = (NSMutableArray *)dataArray2[4];
    mStr = mArr[0];
    [mStr appendString:@"--ONE"];

    NSLog(@"dataArray3:%@",dataArray3);
    NSLog(@"dataArray2:%@",dataArray2);

输出如下:

2016-07-31 17:47:19.421 test1[2174:174714] dataArray3:(  
    one,
    two,
    three,
    four,
        (
        "1--ONE",
        2,
        3,
        4
    )
)
2016-07-31 17:47:19.421 test1[2174:174714] dataArray2:(  
    one,
    two,
    three,
    four,
        (
        "1--ONE",
        2,
        3,
        4
    )
)

可以看到深复制又失效了,这是因为dataArray3=[[NSMutableArray alloc]initWithArray:dataArray2 copyItems:YES];仅仅能进行一层深复制,对于第二层容器的内部对象或者更多层容器的内部对象就无效了,那怎么办呢?

这个时候我们需要完全复制

完全复制

要想对多层集合对象进行复制,我们需要进行完全复制,这里可以使用归档和接档。

实现代码如下:

    dataArray3 = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:dataArray2]];

此时输出如下:

2016-07-31 17:49:55.561 test1[2202:177163] dataArray3:(  
    one,
    two,
    three,
    four,
        (
        1,
        2,
        3,
        4
    )
)
2016-07-31 17:49:55.562 test1[2202:177163] dataArray2:(  
    one,
    two,
    three,
    four,
        (
        "1--ONE",
        2,
        3,
        4
    )
)

可以看到dataArray3没有被dataArray2的修改影响。


类复制

说完了对象的复制,我们来看看如何实现类的复制,因为比较简单,直接放上代码

定义类复制
#import <Foundation/Foundation.h>
@interface Person : NSObject<NSCopying>
@property(strong,nonatomic)NSString *age;
@property(strong,nonatomic)NSString *name;
@end
#import "Person.h"
@implementation Person
- (id)copyWithZone:(NSZone *)zone
{
    Person *person = [[Person allocWithZone:zone] init];
    person.age = self.age;
    person.name = self.name;
    return person;
}
@end
调用
   Person *person = [[Person alloc]init];
        person.age = @"dsdsd";
        person.name = @"dsdsdddww";

        Person *copyPerson = [person copy];
        NSLog(@"%@-----%@",copyPerson.age, copyPerson.name);

可以看到copyPerson的两个属性和persona一样。


@property中的copy关键字

在设置NSString类型的属性的时候,我们最好设置为copy类型,这样别人使用我们定义的属性的时候,他不管怎么改动该属性的赋值,都不会影响我们给该属性赋的值,为什么呢?

下面我们来看看

image

如上图所示,string2的属性是copy类型,可以看到是无法被修改的。

因为此时string2和copystring的内存地址不一样,修改一个,不会影响另外一个。

image

上图所示,如果string2的属性是strong类型,就可以被修改,如下图所示:

因为此时string2和copystring的内存地址都是一样的,修改一个,两个就同时被修改

copy关键字的NSMutableString崩溃

image

原因:

copy关键字的string的setter方法实际上是把参数copy之后再赋值给变量string,那么此时变量string虽然被申明为NSMutableString,但是copy之后,就把 变量_string变成了不可变的NSString类型,所以就会出现方法报错,提示对不可变的NSString使用了NSMutableString的方法appendString。

参考文章:

  1. https://en.wikipedia.org/wiki/Object_copying
  2. http://www.cocoachina.com/bbs/read.php?tid-323045-page-1.html
  3. http://stackoverflow.com/questions/184710/what-is-the-difference-between-a-deep-copy-and-a-shallow-copy
  4. C++的:http://www.fredosaurus.com/notes-cpp/oop-condestructors/shallowdeepcopy.html
  5. .Net的:http://www.codeproject.com/Articles/28952/Shallow-Copy-vs-Deep-Copy-in-NET
  6. Java的:http://javapapers.com/core-java/java-clone-shallow-copy-and-deep-copy/
  7. Generic:https://secweb.cs.odu.edu/~zeil/cs361/web/website/Lectures/big3/pages/shallowvsdeep.html
  8. Objective-C:https://developer.apple.com/library/mac/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/CFMemoryMgmt.pdf
    https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Collections/Collections.pdf
comments powered by Disqus