写在前面
Objective-C中的copy相关内容比我想象中要丰富多了。
使用对象时经常需要拷贝它。在Objective-C中,此操作是通过copy和mutableCopy方法完成的,基类NSObject中与copy相关的API如下:
- (id)copy;
- (id)mutableCopy;
+ (id)copyWithZone:(struct_NSZone *)zone OBJC_ARC_UNAVAILABLE;
+ (id)mutableCopyWithZone:(struct_NSZone *)zone OBJC_ARC_UNAVAILABLE;
显然,NSObject已经实现了copy和mutableCopy方法。
如果想让自己的类(继承自NSObject,假设叫CustomClass)支持拷贝操作,该怎么弄呢?第一个想到的处理方式恐怕是重写copy方法(暂时不谈mutableCopy,稍后再叙)。
但是,这种做法是错误的。
正确的做法是让自定义类遵循NSCopying协议(NSObject并未遵循该协议),该协议只有一个方法:
@protocolNSCopying
- (id)copyWithZone:(NSZone*)zone;
@end
简单来说,当对某个对象发送copy消息时,NSObject#copy的实现逻辑会去自动调用copyWithZone:方法,有点回调的感觉;因此,若想支持拷贝操作,需要在自定义类中让其支持NSCopying,即实现copyWithZone:方法,而不是重写copy方法。
参数zone是什么鬼?这是因为在以前开发程序时,会把内存分为不同的区(zone),而对象会创建在某个区里面。现在不用了,每个程序只有一个区:默认区(default zone)。所以说,尽管必须实现copyWithZone:方法,但是不必担心其中的zone参数。
举个栗子,有个表示个人信息的类,可以在其接口定义中声明此类遵循NSCopying协议,如下:
@interfaceUserInfo:NSObject
@property(nonatomic,copy)NSString*firstName;
@property(nonatomic,copy)NSString*lastName;
- (instancetype)initWithFirstName:(NSString*)firstName
andLastName:(NSString*)lastName;
@end
@implementationUserInfo
- (instancetype)initWithFirstName:(NSString*)firstName
andLastName:(NSString*)lastName {
if(self= [superinit]) {
_firstName = [firstNamecopy];
_lastName = [lastNamecopy];
}
returnself;
}
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone*)zone {
UserInfo *copy= [[[selfclass] allocWithZone:zone] initWithFirstName:_firstName
andLastName:_lastName];
returncopy;
}
@end
再来讲一下mutableCopy方法和NSMutableCopying协议;它们俩与copy方法和NSCopying协议相对应。当你的类还有mutable版本时,你还应该遵循NSMutableCopying协议,并实现mutableCopyWithZone:方法,这样,当向该类对象发送mutableCopy消息时,NSObject的mutableCopy方法实现代码中会回调你的mutableCopyWithZone:方法。
Note:虽然在自定义copyWithZone:和mutableCopyWithZone:中可以弄各种花样,但是务必保证如下逻辑:
[CustomClass copy] -> CustomClass
// 向immutable对象发送copy消息,得到一个immutable对象
[MutableCustomClass copy] -> CustomClass
// 向mutable对象发送一个copy消息,得到一个immutable对象
[CustomClass mutableCopy] -> MutableCustomClass
// 向immutable对象发送mutableCopy消息,得到mutable对象
[MutableCustomClass mutableCopy] -> MutableCustomClass
// 向mutable对象发送mutableCopy消息,得到mutable对象
下一个问题:向immutable对象发送copy消息一定会得到一个新对象吗?
No!下面的测试栗子所做的事情是分别向不可变的NSString、NSArray、NSDictionary以及NSSet对象发送copy消息,得到几个新的对象,新对象显然是immutable的,问题是:这些新对象真的是新对象吗?如下栗子分别把新老对象的地址给打印出来:
NSString*testString =@"1";
NSString*copyString = [testStringcopy];
NSLog(@"testString address = %x", testString);
NSLog(@"copyString address = %x", copyString);
// print:
// testString address = 79720cc0
// copyString address = 79720cc0
NSArray*testArray = @[@1, @2, @3];
NSArray*copyArray = [testArraycopy];
NSLog(@"testArray address = %x", testArray);
NSLog(@"copyArray address = %x", copyArray);
// print:
// testArray address = 79722fb0
// copyArray address = 79722fb0
NSDictionary*testDictionary = @{@1:@2};
NSDictionary*copyDictionary = [testDictionarycopy];
NSLog(@"testDictionary address = %x", testDictionary);
NSLog(@"copyDictionary address = %x", copyDictionary);
// print:
// testDictionary address = 79722fd0
// copyDictionary address = 79722fd0
NSSet*testSet = [NSSetsetWithObject:@1];
NSSet*copySet = [testSetcopy];
NSLog(@"testSet address = %x", testSet);
NSLog(@"copySet address = %x", copySet);
// print:
// testSet address = 79722ff0
// copySet address = 79722ff0
答案很明了!NSString、NSArray、NSDictionary以及NSSet,这是我们最常用的四个含有mutable版本的类型;向这些类型的immutable对象发送copy消息,这些对象会直接返回本身,而不是返回一个新创建的对象。
关于这一点,我反复使用各种姿势测试了很多次,均得到这样的结果;但是目前还没能找到比较权威的说法对这个现象进行说明。不过想想也很容易理解,对于一个immutable对象,真的没必要再复制一个,毕竟其中的内容不会发生改变,如果复制了,那么内存中将会存在两个一模一样的资源,岂不浪费?
总结这一段内容的要点如下:
若想令自己的类具备拷贝功能,则需要遵循NSCopying协议,实现其定义的copyWithZone:方法;
若自定义的类分为immutable和mutable版本,则需要同时遵循NSCopying和NSMutableCopying协议;
向immutable对象发送copy消息,并不一定会得到一个新对象;
在很长时间里,我都认为浅拷贝(shallow copy)指的是「指针拷贝」,而深拷贝(deep copy)才是真正copy一个对象;显然,这种说法是不正确的。
一般来说,「深拷贝」和「浅拷贝」这两个概念是分析集合类型才会谈及的。深拷贝的意思是:在拷贝对象时,将其底层数据也一并复制过去。Foundation框架中所有集合类型在默认情况下都执行浅拷贝,也就是说,只拷贝容器对象本身,而不复制其中数据。这样做的原因在于,容器内的对象未必能拷贝,而且调用者也未必想在拷贝容器时一并拷贝其中每一个对象。
深拷贝和浅拷贝对比图如下:
经常看到@property中有些对象类型属性被strong修饰,有些被copy修饰。strong修饰符的作用不消多说,该如何理解copy修饰符呢?
关于@property中copy修饰符的使用,我曾经历了这么两个阶段:
使用copy修饰mutable类型,使用strong修饰immutable类型;
使用copy修饰immutable类型,使用strong修饰mutable类型;
关于第1个阶段,我忘记了当时是怎么想的,它显然是错的;
关于第2个阶段,我之所以有这样的认识是因为曾在stackoverflow中看到了如下这么一段说明:
For attributes whose type is an immutable value class that conforms to theNSCopyingprotocol, you almost always should specifycopyin your@propertydeclaration. Specifying retain is something you almost never want in such a situation.
这句话错了吗?当然没有,要不也不会得到这么多的votes。但为什么这么说呢?不晓得是当时没耐心还是咋地,反正没怎么思考这个问题。
接着以上文提到的UserInfo为栗子,对之进行简化,只是定义两个NSString属性:firstName和lastName,作为对比前者使用copy修饰,后者使用strong修饰。如下:
@interfaceUserInfo:NSObject
@property(nonatomic,copy)NSString*firstName;
@property(nonatomic,strong)NSString*lastName;
@end
基于UserInfo创建实例进行测试:
NSMutableString*mutableFirstName = [NSMutableStringstringWithFormat:@"李"];
NSMutableString*mutableLastName = [NSMutableStringstringWithFormat:@"四"];
UserInfo *u = [[UserInfo alloc] init];
u.firstName = mutableFirstName;
u.lastName = mutableLastName;
NSLog(@"全名:%@%@", u.firstName, u.lastName);
// print: 全名:李四
// 改mutableFirstName「李」为「公孙」
[mutableFirstName deleteCharactersInRange:NSMakeRange(0,1)];
[mutableFirstName appendString:@"公孙"];
// 改mutableLastName「四」为「四哥」
[mutableLastName appendString:@"哥"];
NSLog(@"全名:%@%@", u.firstName, u.lastName);
// print: 全名:李四哥
简单来说,对于immutable对象类型属性,假设该类型存在mutable版本,若使用strong修饰该属性,则将会是不安全的。
在上述代码中,u.lastName被strong修饰,对之赋值一个mutable类型mutableLastName,之后改变mutableLastName的值(由「四」变为「四哥」),显然也影响到了u.lastName的值,这通常是我们所不希望发生的;作为对比,u.firstName被copy修饰,也为之赋值mutable类型mutableFirstName,之后也改变mutableFirstName的值(由「李」变为「公孙」),但是u.firstName不受影响。
再往深一点看:@property的copy的作用机制是什么?根据我的理解,copy修饰符的意义有两点:
在系统自动合成属性的setter提供「指示」,使用类似于_iVar = [var copy];的方式进行赋值;
告诉使用者,安心的使用吧!
因此,根据我的理解,系统合成UserInfo的firstName和lastName的setter代码如下:
- (void)setFirstName:(NSString*)firstName {
_firstName = [firstNamecopy];
}
- (void)setLastName:(NSString*)lastName {
_lastName = lastName;
}
写到这里,可以回答一个常见的问题了:如何重写带copy关键字的setter?
换句话说,即便firstName属性被copy修饰,但是如果重写setter时采用错误的方式,copy带来的好处会荡然无存。譬如这样重写setFirstName::
- (void)setFirstName:(NSString*)firstName {
_firstName = firstName;
}
得到的结果如下(显然,firstName也是不安全的):
全名:李四
全名:公孙四哥
继续深挖:
是不是所有遵循NSCopying类型属性都应该使用copy修饰呢?
mutable类型属性可以使用copy修饰吗?
对于第一个问题,答案是No!对于向NSString、NSDictionary等属性才需要使用copy修饰,因为它们存在mutable版本,在为属性赋值时,右值很可能是它们的mutable类型对象,若使用strong修饰则会带来不稳定因子;另外一个方面,如果属性类型不存在对应的mutable版本,则完全不用担心这点,反正你也无法在外部修改它,不稳定因子自然不存在了。
对于第二个问题,答案仍然是No!被修饰符copy修饰的属性,默认的setter赋值方式是_iVar = [var copy];而copy方法返回的是immutable类型,将immutable对象赋值给mutable类型指针显然是不对的。
P.S:如果存在mutableCopy修饰符,或许可以使用mutableCopy修饰mutable属性^_^