Copy & MutableCopy
前言
好久都没写东西了,除了平常的工作、学习,过年的喜庆与忙碌....都是借口。
废话不多说,让我们进行今天的话题,Copy 与 MutableCopy。
如何理解Copy
Question
为什么NSArray、NSString、NSDictionary常用copy修饰?
最近有一些同学问过我这个问题,为了弄清这个问题,可能涉及到的东西有点多,我会尽量用比较容易理解的例子和语言来解释。当然,实际编程中,可能会比我所举的例子要复杂的多。
1.什么是 immutable 和 mutable ?
- 在Cocoa的世界中,有一些类分为不可变 (immutable) 和可变 (mutable) 两种版本。比如常见的 NSArray、 NSDictionary、 NSSet、NSString 等,对应关系为(immutable->mutable):
NSArray -> NSMutableArray
NSString -> NSMutableString
NSDictionary-> NSMutableDictionary
** 在设计类的时候,我们应该充分运用属性来封装数据,而如果没有特殊要求,我们的属性对外公开,应该声明为只读,避免在一个类的外部直接修改这个类的属性,这样可以为我们避免很多麻烦的事情。 **
Example
** 我们都知道 NSSet 不会添加重复的数据。我们经常用这个类来实现一些功能,例如一个人的朋友这样的容器。那么,我们来看看,下面这段程序如何来打破 NSSet 的这个特性。 **
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array1 = [@[@1, @2] mutableCopy];
NSMutableArray *array2 = [@[@1] mutableCopy];
NSSet *set = [NSSet setWithObjects:array1, array2, nil];
NSLog(@"Set: %@", set);
}
return 0;
}
** Log: **
2016-02-24 13:34:54.828 NSSet[1392:93361] Set: {(
(
1
),
(
1,
2
)
)}
Program ended with exit code: 0
** 很正常,没什么问题对吧? 看我稍稍加一句代码之后... **
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array1 = [@[@1, @2] mutableCopy];
NSMutableArray *array2 = [@[@1] mutableCopy];
NSSet *set = [NSSet setWithObjects:array1, array2, nil];
[array2 addObject:@2];
NSLog(@"Set: %@", set);
}
return 0;
}
** Log: **
2016-02-24 13:38:11.294 NSSet[1409:94758] Set: {(
(
1,
2
),
(
1,
2
)
)}
Program ended with exit code: 0
** 两个一毛一样的 NSMutableArray 就出现在一个 NSSet容器中了,改变了 NSSet 的原义!!!如果不信,大家可以动手试试。 **
** 说一个常见的问题吧,比如说我们常写的 CHRHomeViewController 有一个 badge 属性,这个属性可能会在 Discovery Profile 等多个模块中修改,如果我们直接改变这个 badge 属性,很可能就会出现 Race Condition(静态条件) 。如果我们把 badge 属性改成只读(readonly) ,然后公开一个 - updateBadge:(NSInteger)badge 方法,然后在本类的内部用队列或者其他技术,我们就可以比较容易的解决这个问题。 **
** 说了这么多,和我们最开始给出的问题有什么关系么? 关系还是有的,对于为什么 NSArray 等属性经常用 copy 来修饰,很大的一个方面就是为了保证,在set之后,我们得到的结果是 immutable 的类型。**
** 由于多态性,父类指针可以指向子类指针, 我们很可能写出这样的代码: **
定义一个User类,User有一个friends属性
@interface CHRUser : NSObject
@property (nonatomic, strong) NSSet <CHRUser *> *friends;
@end
@implementation CHRUser
- (instancetype)init
{
self = [super init];
if (self) {
_friends = [NSSet set];
}
return self;
}
@end
// 我们从服务器上拉取用户chris的朋友们
int main(int argc, const char * argv[]) {
@autoreleasepool {
CHRUser *chris = [CHRUser new];
NSMutableSet *friends = [NSMutableSet set];
/* 从服务器拉取信息 ,解析得到这三个人是chris的朋友..... */
CHRUser *mary = [CHRUser new];
CHRUser *tom = [CHRUser new];
CHRUser *bob = [CHRUser new];
[friends addObject:mary];
[friends addObject:tom];
[friends addObject:bob];
chris.friends = friends;
NSLog(@"Chris' friends: %@", chris.friends);
// 由于某些逻辑,在 friends 中又添加一个User
CHRUser *babara = [CHRUser new];
[friends addObject:babara];
// 使用 friends 去做一些其他的事情
NSLog(@"Chris' friends: %@", chris.friends);
}
return 0;
}
** 我们看看 Log 信息: **
2016-02-24 14:16:24.469 NSSet[1528:109524] Chris' friends: {(
<CHRUser: 0x100209ed0>,
<CHRUser: 0x100209b80>,
<CHRUser: 0x1002096b0>
)}
2016-02-24 14:16:24.469 NSSet[1528:109524] Chris' friends: {(
<CHRUser: 0x100500200>,
<CHRUser: 0x100209b80>,
<CHRUser: 0x100209ed0>,
<CHRUser: 0x1002096b0>
)}
Program ended with exit code: 0
** Chris 的朋友莫名其妙的多了一个,什么情况 ? 因为 CHRUSer 的friends 属性使用的是 strong 关键字修饰,在 setFriends 之后,只是一个引用指向了 friends ,所以如果后面的 friends 进行增删改等操作,那么 chris.friends 就会跟着变咯。 **
** 我们把 friends 属性改用 copy 修饰, 再看 Log: **
2016-02-24 14:27:12.002 NSSet[1565:114822] Chris' friends: {(
<CHRUser: 0x1001037f0>,
<CHRUser: 0x100102f20>,
<CHRUser: 0x1001038c0>
)}
2016-02-24 14:27:12.003 NSSet[1565:114822] Chris' friends: {(
<CHRUser: 0x1001037f0>,
<CHRUser: 0x100102f20>,
<CHRUser: 0x1001038c0>
)}
Program ended with exit code: 0
** 嗯,这通常就是我们想要的结果了, NSDictionary、 NSString 等道理类似。可是为什么会这样呢? **
** 在 Cocoa 的世界中,因为诸如 NSArray 对应有 immutable 和 mutable 两种版本,所以 copy 操作就分成了 copy 和 mutableCopy 两种复制方式。其复制前后结果如下: **
- copy 前后的类型
NSArray copy -> NSArray
NSMutableArray copy -> NSArray
NSString copy -> NSString
NSMutableString copy -> NSString
.....
- mutableCopy 前后的类型
NSArray mutableCopy -> NSMutableArray
NSMutableArray mutableCopy -> NSMutableArray
NSString mutableCopy -> NSMutableString
NSMutableString mutableCopy -> NSMutableString
......
** 可以看出,经过 copy 操作后,得到的总是 immutable 类型, 经过 mutableCopy 操作后, 得到的总是 mutable 类型。 那么这两个操作到底做了什么呢? 代码看一下。 **
- 对 immutable 类进行 copy 和 mutableCopy 操作
int main(int argc, const char * argv[]) {
@autoreleasepool {
CHRUser *user = [CHRUser new];
NSArray *array = @[user];
NSArray *copiedArray = [array copy];
NSMutableArray *mutableCopiedArray = [array mutableCopy];
// 拷贝前
NSLog(@"Before copy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p",
NSStringFromClass([array class]),
array,
array,
array.firstObject);
// copy 之后
NSLog(@"After copy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p",
NSStringFromClass([copiedArray class]),
copiedArray,
copiedArray,
copiedArray.firstObject);
// mutableCopy 之后
NSLog(@"After mutableCopy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p",
NSStringFromClass([mutableCopiedArray class]),
mutableCopiedArray,
mutableCopiedArray,
mutableCopiedArray.firstObject);
}
return 0;
}
** Log: **
2016-02-24 15:15:00.600 NSSet[1764:137701] Before copy : Class:__NSArrayI, Address: 0x1001038e0, Content: (
"<CHRUser: 0x100102470>"
), ContentObject Address: 0x100102470
2016-02-24 15:15:00.601 NSSet[1764:137701] After copy : Class:__NSArrayI, Address: 0x1001038e0, Content: (
"<CHRUser: 0x100102470>"
), ContentObject Address: 0x100102470
2016-02-24 15:15:00.602 NSSet[1764:137701] After mutableCopy : Class:__NSArrayM, Address: 0x100103a60, Content: (
"<CHRUser: 0x100102470>"
), ContentObject Address: 0x100102470
Program ended with exit code: 0
** 可以看出,对 immtable 类进行 copy 、 mutableCopy 得到的结果是不一样得, mutableCopy 之后我们得到了一个 NSArray 的可变版本,并且内存地址与原始的 array 不同,说明对 immutable 类进行 mutableCopy 得到的结果进行了内容拷贝,但是其容器内部的数据并没有拷贝内容(地址没有变化),也就是说,拷贝了一个 壳 ; 而对 immutable 类进行 copy 操作,与原始的 array 是一毛一样, 也就是我们常说的,拷贝了一份指针。**
-
对 mutable 类进行 copy 和 mutableCopy 操作
int main(int argc, const char * argv[]) { @autoreleasepool { CHRUser *user = [CHRUser new]; NSMutableArray *mutableArray = [NSMutableArray arrayWithObject:user]; NSArray *copiedArray = [mutableArray copy]; NSMutableArray *mutableCopiedArray = [mutableArray mutableCopy]; // 拷贝前 NSLog(@"Before copy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p", NSStringFromClass([mutableArray class]), mutableArray, mutableArray, mutableArray.firstObject); // copy 之后 NSLog(@"After copy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p", NSStringFromClass([copiedArray class]), copiedArray, copiedArray, copiedArray.firstObject); // mutableCopy 之后 NSLog(@"After mutableCopy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p", NSStringFromClass([mutableCopiedArray class]), mutableCopiedArray, mutableCopiedArray, mutableCopiedArray.firstObject); } return 0; }
** 我们再来看下 Log **
2016-02-24 15:21:59.657 NSSet[1814:141214] Before copy : Class:__NSArrayM, Address: 0x100509bc0, Content: (
"<CHRUser: 0x100506dd0>"
), ContentObject Address: 0x100506dd0
2016-02-24 15:21:59.658 NSSet[1814:141214] After copy : Class:__NSArrayI, Address: 0x100507450, Content: (
"<CHRUser: 0x100506dd0>"
), ContentObject Address: 0x100506dd0
2016-02-24 15:21:59.658 NSSet[1814:141214] After mutableCopy : Class:__NSArrayM, Address: 0x100509bf0, Content: (
"<CHRUser: 0x100506dd0>"
), ContentObject Address: 0x100506dd0
Program ended with exit code: 0
** 我们发现,对于一个 mutable 类型的对象来说,不论进行的是 copy 还是 mutableCopy 操作,其地址 Address 都不一样, 而其容器中的内容不变。则,对一个 mutable 类型的对象进行 copy 或者是 mutableCopy ,都拷贝了一个壳,而容器内的数据不会进行拷贝内容的操作。 **
** 所以说,我们经常对外开放的属性都是 immutable 的,但因为多态性,总有可能写出诸如 **
self.xxArray = xxMutableArray
** 这样的代码,程序可能会发生难以查明的 bug 。给维护带来灾难。也有的时候,有人会问你, 一个 NSMutableArray 使用 copy 修饰会发生什么, 经过上面的讲解, NSMutableArray copy 后得到的是一个 NSArray 类型的对象, 但是因为声明的是 NSMutableArray , 所以我们调用 addObject: 等 API 时, Xcode 并不会给我们警告,但是运行就会在 调用 addObject: 等 API 处奔溃(unrecognized selector sent to xxx)。 **
** 还有就是,我们要对自身编写的有可变版本的类同时实现两个协议来进行拷贝操作, NSCopying 和 NSMutableCopying 协议, 并在 **
- (id)copyWithZone:(NSZone *)zone;
- (id)mutableCopyWithZone:(NSZone *)zone;
** 两个方法分别返回不可变与可变版本,以供正常使用。 **
浅拷贝 VS 深拷贝
======
这里纯属扩充下知识哈,会的同学别吐槽...
什么是浅拷贝呢?
** 我们在对 immutable 类型的对象进行 copy 时,发现拷贝前后容器的地址不变,这种情况就叫做浅拷贝了。只拷贝一份指针,这个时候改变原数据之后,浅拷贝的数据会跟着变化,因为地址相同嘛。 **
什么是又是深拷贝呢?
** 深拷贝在我来看有一些不同的情况,比如说上面在对 immutable 类型的对象进行 mutableCopy 的时候,容器的地址变了,但是容器内容没变,这也算是深拷贝,不过只是拷贝了一个壳,如果改变其中的内容,那么拷贝后的容器的内容也跟着变化,这种拷贝我称之为 非完全深拷贝(自己起的名字,别喷,哎呀,大家别喷...)。 示例代码如下:**
我们在这里创建两个类,方便观察。一个是 CHRUser 类,有一个 name 读写属性, 和一个 dogs 的只读属性, 在这个类的内部存储着可变的 internalDogs 的读写属性,存储收养的狗狗们; CHRDog 类只有一个简单的 name 属性。 我们来看看如何实现 copy:
-
CHRUser
@class CHRDog; @interface CHRUser : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, copy, readonly) NSSet <CHRDog *> *dogs; - (void)adoptDog:(CHRDog *)dog; @end #import "CHRUser.h" #import "CHRDog.h" @interface CHRUser () @property (nonatomic, strong, readwrite) NSMutableSet <CHRDog *> *internalDogs; @end @implementation CHRUser - (instancetype)init { self = [super init]; if (self) { _internalDogs = [NSMutableSet set]; } return self; } - (NSSet<CHRUser *> *)friends { return [_internalDogs copy]; } - (void)adoptDog:(CHRDog *)dog { [_internalDogs addObject:dog]; } - (NSString *)description { return [NSString stringWithFormat:@"Address: %p\n Name: %@\n InternalDogs: %@", self, _name, _internalDogs]; } #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone { CHRUser *copy = [[CHRUser allocWithZone:zone] init]; copy.name = [_name copy]; copy.internalDogs = [_internalDogs mutableCopy]; return copy; } @end
-
CHRDog
@interface CHRDog : NSObject @property (nonatomic, copy) NSString *name; @end @implementation CHRDog @end
-
main
int main(int argc, const char * argv[]) { @autoreleasepool { CHRDog *dog = [CHRDog new]; dog.name = @"A Dog"; CHRUser *user = [CHRUser new]; user.name = @"Chris"; [user adoptDog:dog]; CHRUser *copy = [user copy]; CHRDog *anotherDog = [CHRDog new]; anotherDog.name = @"Another"; [user adoptDog:anotherDog]; NSLog(@"%@", user); NSLog(@"%@", copy); } return 0; }
-
Log
2016-02-24 18:51:43.102 NSSet[3794:250903] Address: 0x100109880 Name: Chris InternalDogs: {( <CHRDog: 0x100100100>, <CHRDog: 0x1001098d0> )} 2016-02-24 18:51:43.103 NSSet[3794:250903] Address: 0x10010bf70 Name: Chris InternalDogs: {( <CHRDog: 0x1001098d0> )} Program ended with exit code: 0
** 可以看到, copy 前后,内部的变量地址相同,而且原 User 收养第二只狗狗的后, 对于 copy 复本没有影响。但是如果我改变了 internalDogs 的第一个 dog 的话,copy 复本的 internalDogs 还是会受到影响(这里我们用到了, mutable 类型 mutableCopy 后,拷贝了一层壳,还有对外公开只读的 dogs , 而在内部做可变存储的一些方式,解决Race Condition(静态条件)的方式并未给出,方式有很多,大家可以讨论下)。 **
** 这种结果有的时候就是我们想要的,但有的时候我们希望拷贝出来的副本与原始复本完全互相不影响,那么我们就需要用到 完全深拷贝(别打,别打.... 别走,喂,别走啊, 你还是打我吧
:( 。 **
- 完全深拷贝
** 还有一种情况是,不只是拷贝了一个壳,连内容也全部拷贝过来,这种我称之为 完全深拷贝,就是将一个对象的容器属性、变量等一边拷贝,并且将容器中的每一内容都进行拷贝。还是上述代码,只需要改变一下User NSCopying 协议的方法实现,在让 CHRDog 实现NSCopying 协议,我们就可以做到完全深拷贝。 修改如下: **
-
修改后的 CHRDog.m
@implementation CHRDog - (id)copyWithZone:(NSZone *)zone { CHRDog *copy = [[CHRDog allocWithZone:zone] init]; copy.name = _name; return copy; } @end
-
修改后的 CHRUser - copyWithZone:
- (id)copyWithZone:(NSZone *)zone { CHRUser *copy = [[CHRUser allocWithZone:zone] init]; copy.name = [_name copy]; copy.internalDogs = [[NSMutableSet alloc] initWithSet:_internalDogs copyItems:YES]; return copy; }
-
Log
2016-02-24 18:58:32.352 NSSet[3832:253844] Address: 0x1001021e0 Name: Chris InternalDogs: {( <CHRDog: 0x1001024e0>, <CHRDog: 0x1001003c0> )} 2016-02-24 18:58:32.352 NSSet[3832:253844] Address: 0x1001023b0 Name: Chris InternalDogs: {( <CHRDog: 0x1001032e0> )} Program ended with exit code: 0
** 我们看到,修改之后的 Log 文件,原始的 user 与其复本 copy 中的 internalDogs 的地址都不一样, 这样,无论我们怎么修改原始的或者复本,他们都不会互相影响到了。 当然,在真正的项目中遇到的问题会比这复杂的多,不过只要我们掌握好原理,那么就容易解决了。 **
尾声
** 好久没有写新的东西了,今天花了一天的时间,由于时间的关系,后面写的可能有一些匆忙(中间还有人问了一些问题,导致思路断档...),不过只要大家觉得能够学到一些东西,或者我们共同讨论 ,升华一下知识,我都觉得无比的开心和荣耀。 **
** 文章中如果有不对的地方,请指出,我会尽可能快的修改,欢迎 大家来找茬 :)。 **