15.用前缀避免命名空间冲突
Objective-C没有其他语言那种内置的命名空间(namespace)机制。鉴于此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。如果发生命名冲突(naming clash),那么应用程序的链接过程就会出错,因为其中出现了重复符号。(duplicate symbol 开头的)
比无法链接更糟糕的情况是,在运行期载入了含有重命名类的程序库。此时,“动态加载器”(dynamic loader)就遭遇了“重名符号错误”(duplicate symbol error),很可能会令整个应用程序奔溃。
避免此问题的唯一办法就是变相实现命名空间:为所有名称都加上适当前缀。所选前缀可以是与公司、应用程序或二者皆有关联之名。
使用Cocoa创建应用程序时一定要注意,Apple宣称其保留使用所有“两字母前缀”(two-letter prefix)的权利,所以你自己选用的前缀应该是三个字母的。
不仅是类名,应用程序中的所有名称都应加前缀。如果要为既有类新增“分类”(category),那么一定要给“分类”及“分类”中的方法加上前缀。开发者可能会忽视另外一个容易引发命名冲突的地方,那就死类的实现文件中所用的纯C函数及全局变量,这个问题必须要注意。在编译好的目标文件中,这些名称是要算作“顶级符号”(top-level symbol)的。
要点
- 选择与你的公司、应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀。
- 若自己所开发的程序中用到了第三方库,则应为其中的名称加上前缀。
16.提供“全能初始化方法”
把可为对象提供必要信息以便能完成工作的初始化方法叫做“全能初始化方法”(designated initializer)。
如果创建类实例的方式不止一种,那么这个类就会有多个初始化方法。要在其中选定一个作为全能初始化方法,令其他初始化方法都来调用它。NSDate就是个例子,其初始化方法如下:
- (instancetype)init;
- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;;
“initWithTimeIntervalSinceReferenceDate:”是全能初始化方法,其他的初始化方法都要调用它。于是,只有在全能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需修改此方法的代码就好,无须改动其他初始化方法。
比如,要编写一个表示矩形的类。其接口可以这样写:
#import <Foundation/Foundation.h>
@interface EOCRectangle : NSObject
@property(nonatomic,assign,readonly)float width;
@property(nonatomic,assign,readonly)float height;
@end
开发者可能会提供初始化方法以设置这两个属性:
-(instancetype)initWithWidth:(float)width andHeight:(float)height
{
if(self = [super init]){
_width = width;
_height = height;
}
return self;
}
可是,如果有人用[[EOCRectangle alloc]init]来创建矩形,调用完该方法后,全部实力变量都将设为0。不过,我们一般希望自己设定默认的宽度和高度值,或是抛出异常,指明本类实力必须用“全能初始化方法”来初始化:
//Using default values
-(instancetype)init
{
return [self initWithWidth:5.0 andHeight:5.0];
}
//Throwing an exception
-(instancetype)init
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:andHeight: instead." userInfo:nil];
}
设置默认值的那个init方法调用了全能初始化方法。如果采用这个版本来覆写,那么也可以直接在其代码中设置_width与_height实例变量的值。然而,若是类的底层存储方式变了(比如开发者决定把宽度与高度一起放在某结构体中),则init与全能初始化方法设置数据所用的代码就都要修改。在本例这种简单的情况下没有太大问题,但是如果类的初始化方法有很多种,而且待初始化的数据也较为复杂,那么这样做就麻烦得多。很容易就忘了修改其中某个初始化方法,从而导致各初始化方法之间相互不一致。
现在假定要创建名为EOCSquare的类,令其成为EOCRectangle的子类。因为本类表示正方形,所以其宽度和高度必须相等才行。于是,我们可能会像下面这样创建初始化方法:
#import "EOCRectangle.h"
@interface EOCSquare : EOCRectangle
-(instancetype)initWithDimension:(float)dimension;
@end
@implementation EOCSquare
-(instancetype)initWithDimension:(float)dimension
{
return [super initWithWidth:dimension andHeight:dimension];
}
@end
这个方法就是EOCSquare类的全能初始化方法。它调用了超类的全能初始化方法。EOCRectangle类也调用了其超类的全能初始化方法。全能初始化方法的调用链一定要维系。然后,调用者可能会使用“initWithWidth:andHeight:”或init方法来初始化EOCSquare对象。类的编写者并不希望看到此种情况,因为这样做可能会创建出宽度和高度不相等的正方形。于是,就引出了类继承时需要注意的一个重要问题:如果子类的全能初始化方法与超类方法的名称不同,那么总应覆写超类的全能初始化方法。在EOCSquare这个例子中,应该像下面这样覆写EOCRectangle的全能初始化方法:
-(instancetype)initWithWidth:(float)width andHeight:(float)height
{
float dimension = MAX(width, height);
return [self initWithDimension:dimension];
}
覆写这个方法之后,即便使用init来初始化EOCSquare对象,也能照常工作。
有时我们不想覆写超类的全能初始化方法,比方说现在不想令“initWithWidth:andHeight:”方法以其两参数中较大者作边长初始化EOCSquare对象。常用的办法是覆写超类的全能初始化方法并于其中抛出异常:
-(instancetype)initWithWidth:(float)width andHeight:(float)height
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithDimension: instead." userInfo:nil];
}
有时候可能需要编写多个全能初始化方法。比方说,如果某对象的实例有两种完全不同的创建方式,必须分开处理,那么就会出现这种情况。以NSCoding协议为例,此协议定义了下面这个初始化方法,遵从该协议者都应实现此方法:
-(id)initWithCoder:(NSCoder*)decoder;
我们在实现此方法时一般不调用平常所使用的那个全能初始化方法,因为该方法要通过“解码器”(decoder)将对象数据解压缩,所以和普通的初始化方法不同。而且,如果超类也要实现NSCoding,那么还需调用超类“initWithCoder:”方法。于是,子类中不止一个初始化方法调用了超类的初始化方法,因此,严格地说,在这种情况下出现了两个全能初始化方法。
具体到EOCRectangle这个例子上,其代码就是:
#import <Foundation/Foundation.h>
@interface EOCRectangle : NSObject<NSCoding>
@property(nonatomic,assign,readonly)float width;
@property(nonatomic,assign,readonly)float height;
-(instancetype)initWithWidth:(float)width andHeight:(float)height;
@end
@implementation EOCRectangle
//Initializer from NSCoding
-(instancetype)initWithCoder:(NSCoder *)aDecoder
{
//Call through to super's designated initializer
if(self = [super init]){
_width = [aDecoder decodeFloatForKey:@"width"];
_height = [aDecoder decodeFloatForKey:@"height"];
}
return self;
}
NSCoding协议的初始化方法没有调用本类的全能初始化方法,而是调用了超类的相关方法。然而,若超类也实现了NSCoding,则需改为调用超类的”initWithCoder:”初始化方法。例如,EOCSquare类得这么写:
@implementation EOCSquare
//NSCoding designated initializer
-(instancetype)initWithCoder:(NSCoder *)aDecoder
{
if(self = [super initWithCoder:aDecoder]){
//EOCSquare's specific initializer
}
return self;
}
每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上,实现“initWithCoder:”时也要这样,应该先调用超类的相关方法,然后再执行与本类有关的任务。这样编写出来的EOCSquare类就完全遵守NSCoding协议了。如果编写“initWithCoder:”方法时没有调用超类的同名方法,而是调用了自制的初始化方法,或是超类的其他初始化方法,那么EOCRectangle类的“initWithCoder:”方法就没有机会执行,于是,也就无法将_width及_height这两个实例变量解码了。
要点:
- 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
- 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
- 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
17.实现description方法
在新实现的description方法中,也应该像默认的实现那样,打印出类的名字和指针地址,因为这些内容有时也许会用到。有个简单的办法,可以在description中输出很多互不相同的信息,那就是借助NSDictionary类的discription方法。在自定义的description方法中,把待打印的信息放在字典里面,然后将字典对象的description方法所输出的内容包含在字符串里并返回,这样就可以实现精简的信息输出方式了。
例:
-(NSString*)description{
return [NSString stringWithFormat:@“<%@: %p, %@>”,
[self class],
self,
@{@“title”:_title,
@“latitude”:@(_latitude),
@“longitude”:@(_longitude)}];
}
输出格式为:
location = <EOCLocation: 0x7f98f2e01d20, {
latitude = 51.506;
longitude = 0;
title = London;
}>
NSObject协议中还有个方法要注意,那就是debugDescription,此方法的用意与description非常相似。二者的区别在于,debugDescription方法是开发者在调试器中以控制台命令打印对象时才调用的。在NSObject类的默认实现中,此方法只是直接调用了description。如果不想把类名与指针地址这种额外内容放在普通的描述信息里,但是却希望调试的生活能够很方便地看到它们,在这种情况下,就可以重写debugDescription方法。
要点:
- 实现description方法返回一个有意义的字符串,用以描述该实例。
- 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法。
18.尽量使用不可变对象
设计类的时候,应充分运用属性来封装数据。而在使用属性时,则可将其声明为“只读”(read-only)。默认属性是“可读写的”(read-write),这样设计出来的类都是“可变的”(mutable)。
具体到编程实践中,则应该尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。
有时可能想修改封装在对象内部的数据,但是却不想令这些数据为外人所改动。这种情况下,通常做法是在对象内部将readonly属性重新声明为readwrite。当然,如果该属性是nonatomic的,那么这样做可能会产生“竞争条件”(race condition)。在对象内部写入某属性时,对象外的观察者也许正在读取该属性。若想避免此问题,我们可以在必要时通过“派发队列”(dispatch queue)等手段,将所有数据存取操作都设为同步操作。
将属性在对象内部重新声明为readwrite这一操作可于“class-continuation分类”中完成,在公共接口中声明的属性可于此处重新声明,属性的其他特质必须保持不变,而readonly可扩展为readwrite。
例:
#import <Foundation/Foundation.h>
@interface EOCPointOfInterest : NSObject
@property(nonatomic,copy,readonly)NSString *identifier;
@property(nonatomic,copy,readonly)NSString *title;
@property(nonatomic,assign,readonly)float latitude;
@property(nonatomic,assign,readonly)float longitude;
-(instancetype)initWithIdentifier:(NSString *)identifier
title:(NSString *)title
latitude:(float)latitude
longitude:(float)longitude;
@end
#import "EOCPointOfInterest.h"
@interface EOCPointOfInterest()
@property(nonatomic,copy,readwrite)NSString *identifier;
@property(nonatomic,copy,readwrite)NSString *title;
@property(nonatomic,assign,readwrite)float latitude;
@property(nonatomic,assign,readwrite)float longitude;
@end
@implementation EOCPointOfInterest
@end
现在,只能于EOCPointOfInterest实现代码内部设置这些属性值了。
在定义类的公共API时,还要注意一件事情:对象里表示各种collection的那些属性究竟应该设成可变的,还是不可变的。例如,我们用某个类来表示个人信息,该类里面还存放了一些引用,指向此人的诸位朋友。你可能想把这个人的全部朋友都放在一个列表里,并将其做成属性。假如开发者可以添加或删除此人的朋友,那么这个属性就需要用可变的set来实现。在这种情况下,通常应该提供一个readonly属性供外界使用,该属性将返回不可变的set,而此set则是内部那个可变set的一份拷贝。比方说,下面这段代码就能够实现这样一个类:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property(nonatomic,copy,readonly)NSString *firstName;
@property(nonatomic,copy,readonly)NSString *lastName;
@property(nonatomic,strong,readonly)NSSet *friends;
-(instancetype)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName;
-(void)addFriend:(EOCPerson *)person;
-(void)removeFriend:(EOCPerson *)person;
@end
#import "EOCPerson.h"
@interface EOCPerson()
@property(nonatomic,copy,readwrite)NSString *firstName;
@property(nonatomic,copy,readwrite)NSString *lastName;
@end
@implementation EOCPerson
{
NSMutableSet *_internalFriends;
}
-(NSSet *)friends
{
return [_internalFriends copy];
}
-(void)addFriend:(EOCPerson *)person
{
[_internalFriends addObject:person];
}
-(void)removeFriend:(EOCPerson *)person
{
[_internalFriends removeObject:person];
}
-(instancetype)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName
{
if(self = [super init]){
_firstName = firstName;
_lastName = lastName;
_internalFriends = [NSMutableSet new];
}
return self;
}
@end
也可以用NSMutableSet来实现friends属性,令该类的用户不借助“addFriend:”与“removeFriend:”方法而直接操作此属性。但是,这种过分解耦数据的做法很容易出bug。比方说,在添加或删除朋友时,EOCPerson对象可能还要执行其他相关操作,若是采用这种做法,那就等于直接从底层修改了其内部用于存放朋友对象的set。在EOCPerson对象不知情时,直接从底层修改set可能会令对象内的各数据之间互不一致。
不要在返回的对象上查询类型以确定其是否可变。比方说,你正在使用一个包含EOCPerson类的库来开发程序。为了省事,该库的开发者可能并没有将内部那个可变的set拷贝一份再返回,而是直接返回了可变了的set。这样做也算合理,因为set可能很大,拷贝起来太耗时了。返回NSMutableSet也合乎语法,因为该类是NSSet的子类,于是,你可能会像这样来使用EOCPerson:
EOCPerson *person = /*...*/;
NSSet *friends = person.friends;
if([friends isKindOfClass:[NSMutableSet class]]){
NSMutableSet *mutableFriends = (NSMutableSet *)friends;
/*mutate the set*/
}
大家应该竭力避免这种做法。在你与EOCPerson类之间的约定里,并没有提到实现friends所用的那个NSSet一定是可变的,因此不应像这样使用类型信息查询功能来编码。这依然说明:开发者或许不宜从底层直接修改对象中的数据。所以,不要假设这个NSSet就一定能直接修改。
要点:
- 尽量创建不可变的对象。
- 若某属性仅可于对象内部修改,则在“class-continuation分类”中将其由readonly属性扩展为readwrite属性。
- 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。
19.使用清晰而协调的命名方式
-
给方法命名的规则:
- 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型。除非前面还有修饰语,例如localizedString。属性的存取方法不遵守这种命名方式,因为一般认为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其所对应的属性来命名。
- 应该把表示参数类型的名词放在参数前面。
- 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。
- 不要使用str这种简称,应该用string这样的全称。
- Boolean属性应加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功能,选用has或is当前缀。
- 将get这个前缀留给那些借由“输出参数”来保存返回值的方法,比如说,把返回值填充到“C语言式数组”里的那种方法就可以使用这个词做前缀。
-
类和协议的命名:
- 应该为类与协议的名称加上前缀,以避免命名空间冲突,而且应该像给方法起名时那样把词句组织好,使其从左至右读起来较为通顺。如果要从其他框架中继承子类,那么务必遵循其命名惯例。比方说,要从UIView类中继承自定义的子类,那么类名末尾的词必须是View。同理,若要创建自定义的委托协议,则其名称中应该包含委托发起方的名称,后面再跟上Delegate一词。
要点:
- 起名时应遵从标准的Objective-C命名规范,这样创建出来的接口更容易为开发者所理解。
- 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
- 方法名里不要使用缩略后的类型名称。
- 给方法起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。
20.为私有方法名加前缀
要点:
- 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
- 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。
21.理解Objective-C错误模型
“自动引用计数”(ARC)在默认情况下不是“异常安全”(exception safe)。这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptions。
即使不用ARC,也很难写出在抛出异常时不会导致内存泄露的代码。比方说,设有段代码先创建好了某个资源,使用完之后再将其释放。可是,在释放资源之前如果抛出异常了,那么该资源就不会被释放了:
id someResource = /*...*/;
if(/*check for error*/){
@throw [NSException exceptionWithName:@"ExceptionName" reason:@"There was an error" userInfo:nil];
}
[someResource doSomething];
[someResource release];
在抛出异常之前先释放someResource,这样做当然能解决此问题,不过要待释放的资源有很多,而且代码的执行路径更为复杂的话,那么释放资源的代码就容易写得很乱。此外,代码中加入了新的资源之后,开发者经常会忘记在抛出异常前先把它释放掉。
Objective-C语言现在所采用的办法是:只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说,不用再编写复杂的“异常安全”代码了。
异常只应该用于极其严重的错误,比方说,你编写了某个抽象基类,它的正确用法是先从中继承一个子类,然后使用这个子类。在这种情况下,如果有人直接使用了这个抽象基类,那么可以考虑抛出异常。与其他语言不同,Objective-C中没办法将其某个类标识为“抽象类”。要想达成类似效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。这样的话,只要有人直接创建抽象基类的实例并使用它,即会抛出异常:
-(void)mustOverrrideMethod{
NSString *reason = [NSString stringWithFormat:@"%@ must be overrideden",NSStringFromSelector(_cmd)];
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil];
}
```
在出现“不那么严重的错误”(nonfatal error,非致命错误)时,Objective-C语言所用的编程范式为:令方法返回nil/0,或是使用NSError,以表明其中有错误发生。比方说,如果初始化方法无法根据传入的参数来初始化当前实例,那么就可以令其返回nil/0:
```
-(instancetype)initWithValue:(id)value{
if(self = [super init]){
if(/*Value means instance can't be created*/){
self = nil;
}else{
//Initialze instance
}
}
return self;
}
```
在这种情况下,如果if语句发现无法用传入的参数值来初始化当前实例(比如这个方法要求传入的value参数必须是non-nil的),那么就把self设置成nil,这样的话,整个方法的返回值也就是nil了。调用者发现初始化方法并没有把实例创建好,于是便可确定其中发生了错误。
NSError的用法更加灵活,因为经由此对象,我们可以把导致错误的原因回报给调用者。NSError对象里封装了三条信息:
* Error domain(错误范围,其类型为字符串)
错误发生的范围。也就是产生错误的根源,通常用一个特有的全局变量来定义。比方说,“处理URL的子系统”(URL-handling subsystem)在从URL中解析或获得数据时如果出错了,那么就会使用NSURLErrorDomain来表示错误范围。
* Error code(错误码,其类型为整数)
独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围内可能会发生一系列相关错误,这些错误情况通常采用enum来定义。例如,当HTTP请求出错时,可能会把HTTP状态码设为错误码。
* User info(用户信息,其类型为字典)
有关此错误的额外信息,其中或许包含一段“本地化的描述”(localized description),或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条“错误链”(chain of errors)。
在设计API时,NSError的第一种常见用法是通过委托协议来传递此错误。有错误发生时,当前对象会把错误信息经由此协议中的某个方法传给其委托对象(delegate)。例如,NSURLConnection在其委托协议NSURLConnectionDelegate之中就定义了如下方法:
```
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
```
当NSURLConnection出错之后,就会调用此方法以处理相关错误。这个委托方法未必非得实现不可:是不是必须处理此错误,可交由NSURLConnection类的用户来判断。这比抛出异常要好,因为调用者至少可以自己决定NSURLConnection是否回报此错误。
NSError的另外一种常见的用法是:经由方法的“输出参数”返回给调用者。比如像这样:- (BOOL)doSomething:(NSString**)error
传递给方法的参数是个指针,而该指针本身又指向另外一个指针,那个指针指向NSError对象。或者也可以把它当成一个直接指向NSError对象的指针。这样一来,此方法不仅能有普通的返回值,而且还能经由“输出参数”把NSError对象回传给调用者。其方法如下:
```
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if(error){
//There was an error
}
```
像这样的方法一般都会返回Boolean值,用以表示该操作是成功了还是失败了。如果调用者不关注具体的错误信息,那么直接判断这个Boolean值就好;若是关注具体错误,那就检查经由“输出参数”所返回的那个错误对象。在不想知道具体错误的时候,可以给error参数传入nil。
实际上,在使用ARC时,编译器会把方法签名中的NSError**转换成NSError*__autoreleasing*,也就是说,指针所指的对象会在方法执行完毕后自动释放。这个对象必须自动释放,因为”doSomething:"方法不能保证其调用者可以把此方法中创建的NSError释放掉,所以必须加入autorelease。这就与大部分方法(以new、alloc、copy、mutableCopy开头的方法当然不在此列)的返回值所具备的的语义相同了。
该方法通过下列代码把NSError对象传递到“输出参数”中:
```
- (BOOL)doSomething:(NSString**)error{
//Do something that may cause an error
if(/*there was an error*/){
if(error){
//Pase the 'error' through the out-parameter
*error = [NSError errorWithDomain:domain code:code userInfo:userinfo];
}
return NO;///< Indicate failure
}else{
return YES;///< Indicate success
}
}
```
这段代码以*error语法为error参数“解引用”(dereference),也就是说,error所指的那个指针现在要向一个新的NSError对象了。在解引用之前,必须先保证error参数不是nil,因为空指针解引用会导致“段错误”(segmentation fault)并使应用程序崩溃。调用者在不关心具体错误时,会给error参数传入nil,所以必须判断这种情况。
NSError对象里的“错误范围”、“错误码”、“用户信息”等部分应该按照具体的错误情况填入适当内容。这样的话,调用者就可以根据错误类型分别处理各种错误了。错误范围应该定义成NSString类型的全局常量,而错误码则定义成枚举类型为佳。
最好能为你自己的程序库中所发生的错误指定一个专用的“错误范围”字符串,使用此字符串创建NSError对象,并将其返回给库的使用者,这样的话,他们就能确信:该错误肯定是由你的程序库所回报的。用枚举类型来表示错误码也是明智之举,因为这些枚举不仅解释了错误码的含义,而且还给它们起了个有意义的名字。此外,也可以在定义这些枚举的头文件里对每个错误类型祥加说明。
**要点:**
* **只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。**
* **在错误不那么严重的情况下,可以指派“委托方法”来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。**
##22.理解NSCopying协议
如果想令自己的类支持拷贝操作,那就要实现NSCopying协议,该协议只有一个方法:
```
- (id)copyWithZone:(NSZone*)zone
```
因为以前开发程序时,会据此把内存分成不同的“区”(zone)而对象会创建在某个区里面。现在不用了,每个程序只有一个区:“默认区”(default zone)。所以说,尽管必须实现这个方法,但是不必担心其中的zone参数。
**copy方法由NSObject实现,该方法只是以“默认区”为参数来调用“copyWithZone:”。我们只是想覆写copy方法,其实真正需要实现的确实“copyWithZone:”方法。**
比方说,有个表示个人信息的类,可以在其接口定义中声明此类遵从NSCopying协议:
```
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject<NSCopying>
@property(nonatomic,copy,readonly)NSString *firstName;
@property(nonatomic,copy,readonly)NSString *lastName;
-(instancetype)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName;
@end
然后,实现协议中规定的方法:
-(id)copyWithZone:(NSZone *)zone
{
EOCPerson *copy = [[[self class]allocWithZone:zone]
initWithFirstName:_firstName
andLastName:_lastName];
return copy;
}
```
在本例所实现的“copyWithZone:”中,我们直接把待拷贝的对象交给“全能初始化方法”(designated initializer),令其执行所有初始化工作。然而有的时候,除了要拷贝对象,还要完成其他一些操作,比如类对象中的数据结构可能并未在初始化方法中设置好,需要另行设置。举个例子,加入EOCPerson中含有一个数组,与其他EOCPerson对象简历或解除朋友关系的那些方法都需要操作这个数组。那么在这种情况下,你得把这个包含朋友对象的数组也一并拷贝过来。下面列出了实现此功能所需的全部代码:
```
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject<NSCopying>
@property(nonatomic,copy,readonly)NSString *firstName;
@property(nonatomic,copy,readonly)NSString *lastName;
@property(nonatomic,strong,readonly)NSSet *friends;
-(instancetype)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName;
-(void)addFriend:(EOCPerson *)person;
-(void)removeFriend:(EOCPerson *)person;
@end
@implementation EOCPerson
{
NSMutableSet *_friends;
}
-(void)addFriend:(EOCPerson *)person
{
[_friends addObject:person];
}
-(void)removeFriend:(EOCPerson *)person
{
[_friends removeObject:person];
}
-(instancetype)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName
{
if(self = [super init]){
_firstName = firstName;
_lastName = lastName;
_friends = [NSMutableSet new];
}
return self;
}
-(id)copyWithZone:(NSZone *)zone
{
EOCPerson *copy = [[[self class]allocWithZone:zone]
initWithFirstName:_firstName
andLastName:_lastName];
copy->_friends = [_friends mutableCopy];
return copy;
}
@end
```
这次所实现的方法比原来多了一些代码,它把本对象的_friends实例变量复制了一份,令copy对象的_friends实例变量指向这个复制过的set。**注意,这里使用了->语法,因为_friends并非属性,只是个在内部使用的实例变量。**
通常情况下,应该像本例这样,采用全能初始化方法来初始化待拷贝的对象。不过有些时候不能这么做,因为全能初始化方法会产生一些“副作用”,这些附加操作对目前要拷贝的对象无益。比如,初始化方法可能要设置一个复杂的内部数据结构,可是在拷贝的对象中,这个数据结构立刻就要用其他数据来覆写,所以没必要再设置一遍。
仔细看看刚才“copyWithZone:”方法,你就会发现,存放朋友对象的那个set是通过mutableCopy方法来赋值的。此方法来自另一个叫做NSMutableCopying的协议。该协议与NSCopying类似,也只定义了一个方法:
```
- (id)mutableCopyWithZone:(NSZone*)zone
```
mutableCopy这个“辅助方法”与copy相似,也是用默认的zone参数来调“mutableCopyWithZone:”。如果你的类分为可变版本和不可变版本,那么就应该实现NSMutableCopying。若采用此模式,则在可变类中覆写“copyWithZone:”方法时,不要返回可变的拷贝,而应返回一份不可变的版本。无论当前实例是否可变,若需获取其可变版本的拷贝,均应调用mutableCopy方法。同理,若需要不可变的拷贝,则总应通过copy方法来获取。
对于不可变的NSArray与可变的NSMutableArray来说,下列关系总是成立的:
```
-[NSMutableArray copy] => NSArray
-[NSArray mutableCopy] => NSMutableArray
```
在可变对象上调用copy方法会返回另外一个不可变类的实例。这样做是为了能在可变版本和不可变版本之间自由切换。
**在编写拷贝方法时,还要决定一个问题,就是应该执行“深拷贝”(deep copy)还是“浅拷贝”(shallow copy)。深拷贝的意思就是:在拷贝对象自身时,将其底层数据也一并复制过去。Foundation框架中的所有collection类在默认情况下都执行浅拷贝,也就是说,只拷贝容器对象本身,而不复制其中数据。这样做的主要原因在于,容器内的对象未必都能拷贝,而且调用者也未必想在拷贝容器时一并拷贝其中的每个对象。**深拷贝与浅拷贝的区别如下图:
![深拷贝与浅拷贝](https://img.haomeiwen.com/i141604/5922e05c349e2aad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
一般情况下,我们会遵照系统框架所使用的那种模式,在自定义的类中以浅拷贝的方式实现"copyWithZone:”方法。但如果有必要的话,也可以增加一个执行深拷贝的方法。以NSSet为例,该类提供了下面这个初始化方法,用以执行深拷贝:
```
- (id)initWithSet:(NSArray *)array copyItems:(BOOL)copyItems
```
若copyItems参数设为YES,则该方法会向数组中的每个元素发送copy消息,用拷贝好的元素创建新的set,并将其返回给调用者。
在EOCPerson那个例子中,存放朋友对象的set是用”copyWithZone:”方法来拷贝的,根据刚才讲的内容可知,这种浅拷贝方式不会逐个赋值set中的元素。若需要深拷贝的话,则可像下面这样,编写一个专供深拷贝所用的方法:
```
-(id)deepCopy
{
EOCPerson *copy = [[[self class]alloc]
initWithFirstName:_firstName
andLastName:_lastName];
copy->_friends = [[NSMutableSet alloc]initWithSet:_friends
copyItems:YES];
return copy;
}
```
因为没有专门定义深拷贝的协议,所以其具体执行方式由每个类来确定,你只需要决定自己所写的类是否要提供深拷贝方法即可。另外,不要假定遵从了NSCopying协议的对象都会执行深拷贝。在绝大多数情况下,执行的都是浅拷贝。如果需要在某对象上执行深拷贝,那么除非该类的文档说它是用深拷贝来实现NSCopying协议的,否则,要么寻找能执行深拷贝的相关方法,要么自己编写方法来做。
**要点:**
* **若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。**
* **如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议。**
* **复制对象时需决定采用深拷贝还是浅拷贝,一般情况下应该尽量执行浅拷贝。**
* **如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。**
转载请注明出处:[第三章 接口与API设计](http://www.jianshu.com/p/56aeaf3ba553)
_参考:《Effective Objective-C 2.0》_
网友评论