Copy & MutableCopy
前言
好久都沒(méi)寫東西了,除了平常的工作吧趣、學(xué)習(xí),過(guò)年的喜慶與忙碌....都是借口俯渤。
廢話不多說(shuō),讓我們進(jìn)行今天的話題,Copy 與 MutableCopy岖寞。
如何理解Copy
Question
為什么NSArray指巡、NSString、NSDictionary常用copy修飾?
最近有一些同學(xué)問(wèn)過(guò)我這個(gè)問(wèn)題,為了弄清這個(gè)問(wèn)題椭住,可能涉及到的東西有點(diǎn)多京郑,我會(huì)盡量用比較容易理解的例子和語(yǔ)言來(lái)解釋些举。當(dāng)然,實(shí)際編程中,可能會(huì)比我所舉的例子要復(fù)雜的多鸠信。
1.什么是 immutable 和 mutable 星立?
- 在Cocoa的世界中,有一些類分為不可變 (immutable) 和可變 (mutable) 兩種版本辕坝。比如常見(jiàn)的 NSArray酱畅、 NSDictionary纺酸、 NSSet碎紊、NSString 等,對(duì)應(yīng)關(guān)系為(immutable->mutable):
NSArray -> NSMutableArray
NSString -> NSMutableString
NSDictionary-> NSMutableDictionary
** 在設(shè)計(jì)類的時(shí)候,我們應(yīng)該充分運(yùn)用屬性來(lái)封裝數(shù)據(jù)顿膨,而如果沒(méi)有特殊要求,我們的屬性對(duì)外公開顾画,應(yīng)該聲明為只讀,避免在一個(gè)類的外部直接修改這個(gè)類的屬性,這樣可以為我們避免很多麻煩的事情末誓。 **
Example
** 我們都知道 NSSet 不會(huì)添加重復(fù)的數(shù)據(jù)喇澡。我們經(jīng)常用這個(gè)類來(lái)實(shí)現(xiàn)一些功能,例如一個(gè)人的朋友這樣的容器呕屎。那么尔当,我們來(lái)看看,下面這段程序如何來(lái)打破 NSSet 的這個(gè)特性畜号。 **
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
** 很正常药蜻,沒(méi)什么問(wèn)題對(duì)吧贸典? 看我稍稍加一句代碼之后... **
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
** 兩個(gè)一毛一樣的 NSMutableArray 就出現(xiàn)在一個(gè) NSSet容器中了廊驼,改變了 NSSet 的原義N鞫F谙骸!如果不信茂蚓,大家可以動(dòng)手試試御板。 **
** 說(shuō)一個(gè)常見(jiàn)的問(wèn)題吧淹朋,比如說(shuō)我們常寫的 CHRHomeViewController 有一個(gè) badge 屬性杈抢,這個(gè)屬性可能會(huì)在 Discovery Profile 等多個(gè)模塊中修改,如果我們直接改變這個(gè) badge 屬性歼捐,很可能就會(huì)出現(xiàn) Race Condition(靜態(tài)條件) 淘这。如果我們把 badge 屬性改成只讀(readonly) 钠怯,然后公開一個(gè) - updateBadge:(NSInteger)badge 方法晦炊,然后在本類的內(nèi)部用隊(duì)列或者其他技術(shù)刽锤,我們就可以比較容易的解決這個(gè)問(wèn)題并思。 **
** 說(shuō)了這么多宋彼,和我們最開始給出的問(wèn)題有什么關(guān)系么输涕? 關(guān)系還是有的莱坎,對(duì)于為什么 NSArray 等屬性經(jīng)常用 copy 來(lái)修飾碴卧,很大的一個(gè)方面就是為了保證住册,在set之后荧飞,我們得到的結(jié)果是 immutable 的類型叹阔。**
** 由于多態(tài)性忠荞,父類指針可以指向子類指針, 我們很可能寫出這樣的代碼: **
定義一個(gè)User類堂油,User有一個(gè)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
// 我們從服務(wù)器上拉取用戶chris的朋友們
int main(int argc, const char * argv[]) {
@autoreleasepool {
CHRUser *chris = [CHRUser new];
NSMutableSet *friends = [NSMutableSet set];
/* 從服務(wù)器拉取信息 ,解析得到這三個(gè)人是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 中又添加一個(gè)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 的朋友莫名其妙的多了一個(gè)系宜,什么情況 盹牧? 因?yàn)?CHRUSer 的friends 屬性使用的是 strong 關(guān)鍵字修飾口柳,在 setFriends 之后有滑,只是一個(gè)引用指向了 friends 跃闹,所以如果后面的 friends 進(jìn)行增刪改等操作,那么 chris.friends 就會(huì)跟著變咯毛好。 **
** 我們把 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
** 嗯,這通常就是我們想要的結(jié)果了睛榄, NSDictionary荣茫、 NSString 等道理類似〕⊙ィ可是為什么會(huì)這樣呢啡莉? **
** 在 Cocoa 的世界中,因?yàn)橹T如 NSArray 對(duì)應(yīng)有 immutable 和 mutable 兩種版本,所以 copy 操作就分成了 copy 和 mutableCopy 兩種復(fù)制方式哮兰。其復(fù)制前后結(jié)果如下: **
- 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
......
** 可以看出膏秫,經(jīng)過(guò) copy 操作后,得到的總是 immutable 類型, 經(jīng)過(guò) mutableCopy 操作后婿斥, 得到的總是 mutable 類型活鹰。 那么這兩個(gè)操作到底做了什么呢? 代碼看一下。 **
- 對(duì) immutable 類進(jìn)行 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
** 可以看出遍希,對(duì) immtable 類進(jìn)行 copy 、 mutableCopy 得到的結(jié)果是不一樣得别厘, mutableCopy 之后我們得到了一個(gè) NSArray 的可變版本,并且內(nèi)存地址與原始的 array 不同,說(shuō)明對(duì) immutable 類進(jìn)行 mutableCopy 得到的結(jié)果進(jìn)行了內(nèi)容拷貝颈畸,但是其容器內(nèi)部的數(shù)據(jù)并沒(méi)有拷貝內(nèi)容(地址沒(méi)有變化)徙缴,也就是說(shuō),拷貝了一個(gè) 殼 糊余; 而對(duì) immutable 類進(jìn)行 copy 操作,與原始的 array 是一毛一樣, 也就是我們常說(shuō)的,拷貝了一份指針句柠。**
-
對(duì) mutable 類進(jìn)行 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; }
** 我們?cè)賮?lái)看下 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
** 我們發(fā)現(xiàn),對(duì)于一個(gè) mutable 類型的對(duì)象來(lái)說(shuō),不論進(jìn)行的是 copy 還是 mutableCopy 操作,其地址 Address 都不一樣, 而其容器中的內(nèi)容不變挠锥。則蓖宦,對(duì)一個(gè) mutable 類型的對(duì)象進(jìn)行 copy 或者是 mutableCopy 睬关,都拷貝了一個(gè)殼丐箩,而容器內(nèi)的數(shù)據(jù)不會(huì)進(jìn)行拷貝內(nèi)容的操作涎嚼。 **
** 所以說(shuō)立哑,我們經(jīng)常對(duì)外開放的屬性都是 immutable 的产喉,但因?yàn)槎鄳B(tài)性姐帚,總有可能寫出諸如 **
self.xxArray = xxMutableArray
** 這樣的代碼,程序可能會(huì)發(fā)生難以查明的 bug 。給維護(hù)帶來(lái)災(zāi)難。也有的時(shí)候柔纵,有人會(huì)問(wèn)你搁料, 一個(gè) NSMutableArray 使用 copy 修飾會(huì)發(fā)生什么昭伸, 經(jīng)過(guò)上面的講解夹供, NSMutableArray copy 后得到的是一個(gè) NSArray 類型的對(duì)象匪凉, 但是因?yàn)槁暶鞯氖?NSMutableArray , 所以我們調(diào)用 addObject: 等 API 時(shí)积瞒, Xcode 并不會(huì)給我們警告叮喳,但是運(yùn)行就會(huì)在 調(diào)用 addObject: 等 API 處奔潰(unrecognized selector sent to xxx)。 **
** 還有就是缰贝,我們要對(duì)自身編寫的有可變版本的類同時(shí)實(shí)現(xiàn)兩個(gè)協(xié)議來(lái)進(jìn)行拷貝操作馍悟, NSCopying 和 NSMutableCopying 協(xié)議, 并在 **
- (id)copyWithZone:(NSZone *)zone;
- (id)mutableCopyWithZone:(NSZone *)zone;
** 兩個(gè)方法分別返回不可變與可變版本剩晴,以供正常使用锣咒。 **
淺拷貝 VS 深拷貝
======
這里純屬擴(kuò)充下知識(shí)哈,會(huì)的同學(xué)別吐槽...
什么是淺拷貝呢?
** 我們?cè)趯?duì) immutable 類型的對(duì)象進(jìn)行 copy 時(shí)宠哄,發(fā)現(xiàn)拷貝前后容器的地址不變壹将,這種情況就叫做淺拷貝了嗤攻。只拷貝一份指針毛嫉,這個(gè)時(shí)候改變?cè)瓟?shù)據(jù)之后,淺拷貝的數(shù)據(jù)會(huì)跟著變化妇菱,因?yàn)榈刂废嗤铩?**
什么是又是深拷貝呢承粤?
** 深拷貝在我來(lái)看有一些不同的情況,比如說(shuō)上面在對(duì) immutable 類型的對(duì)象進(jìn)行 mutableCopy 的時(shí)候闯团,容器的地址變了辛臊,但是容器內(nèi)容沒(méi)變,這也算是深拷貝房交,不過(guò)只是拷貝了一個(gè)殼彻舰,如果改變其中的內(nèi)容,那么拷貝后的容器的內(nèi)容也跟著變化候味,這種拷貝我稱之為 非完全深拷貝(自己起的名字刃唤,別噴,哎呀白群,大家別噴...)尚胞。 示例代碼如下:**
我們?cè)谶@里創(chuàng)建兩個(gè)類,方便觀察帜慢。一個(gè)是 CHRUser 類笼裳,有一個(gè) name 讀寫屬性, 和一個(gè) dogs 的只讀屬性粱玲, 在這個(gè)類的內(nèi)部存儲(chǔ)著可變的 internalDogs 的讀寫屬性躬柬,存儲(chǔ)收養(yǎng)的狗狗們; CHRDog 類只有一個(gè)簡(jiǎn)單的 name 屬性抽减。 我們來(lái)看看如何實(shí)現(xiàn) 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 前后,內(nèi)部的變量地址相同胯甩,而且原 User 收養(yǎng)第二只狗狗的后昧廷, 對(duì)于 copy 復(fù)本沒(méi)有影響。但是如果我改變了 internalDogs 的第一個(gè) dog 的話偎箫,copy 復(fù)本的 internalDogs 還是會(huì)受到影響(這里我們用到了木柬, mutable 類型 mutableCopy 后,拷貝了一層殼淹办,還有對(duì)外公開只讀的 dogs 眉枕, 而在內(nèi)部做可變存儲(chǔ)的一些方式,解決Race Condition(靜態(tài)條件)的方式并未給出,方式有很多速挑,大家可以討論下)谤牡。 **
** 這種結(jié)果有的時(shí)候就是我們想要的,但有的時(shí)候我們希望拷貝出來(lái)的副本與原始復(fù)本完全互相不影響姥宝,那么我們就需要用到 完全深拷貝(別打翅萤,別打.... 別走,喂腊满,別走啊套么, 你還是打我吧
:( 。 **
- 完全深拷貝
** 還有一種情況是碳蛋,不只是拷貝了一個(gè)殼胚泌,連內(nèi)容也全部拷貝過(guò)來(lái),這種我稱之為 完全深拷貝肃弟,就是將一個(gè)對(duì)象的容器屬性玷室、變量等一邊拷貝,并且將容器中的每一內(nèi)容都進(jìn)行拷貝笤受。還是上述代碼穷缤,只需要改變一下User NSCopying 協(xié)議的方法實(shí)現(xiàn),在讓 CHRDog 實(shí)現(xiàn)NSCopying 協(xié)議感论,我們就可以做到完全深拷貝绅项。 修改如下: **
-
修改后的 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 與其復(fù)本 copy 中的 internalDogs 的地址都不一樣快耿, 這樣,無(wú)論我們?cè)趺葱薷脑嫉幕蛘邚?fù)本芳绩,他們都不會(huì)互相影響到了掀亥。 當(dāng)然,在真正的項(xiàng)目中遇到的問(wèn)題會(huì)比這復(fù)雜的多妥色,不過(guò)只要我們掌握好原理搪花,那么就容易解決了。 **
尾聲
** 好久沒(méi)有寫新的東西了嘹害,今天花了一天的時(shí)間撮竿,由于時(shí)間的關(guān)系,后面寫的可能有一些匆忙(中間還有人問(wèn)了一些問(wèn)題笔呀,導(dǎo)致思路斷檔...)幢踏,不過(guò)只要大家覺(jué)得能夠?qū)W到一些東西,或者我們共同討論 许师,升華一下知識(shí)房蝉,我都覺(jué)得無(wú)比的開心和榮耀僚匆。 **
** 文章中如果有不對(duì)的地方,請(qǐng)指出搭幻,我會(huì)盡可能快的修改咧擂,歡迎 大家來(lái)找茬 :)。 **