第15条:用前缀避免命名空间冲突
OC没有其他语言那种内置的命名空间(namespace)机制,鉴于此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。
避免重名的唯一办法就是变相实现命名空间:为所有名称都加上适当的前缀。
使用Cocoa创建应用程序时要注意,Apple宣称其保留使用所有“两个字母前缀”(two-letter prefix)的权利,所以你自己选用的前缀应该是三个字母的。
不仅仅是类名,应用程序中的所有名称都应加前缀,如果要为既有类新增分类,那么一点要给分类及分类中的方法加上前缀。
类实现文件中所用的纯C函数及全局变量也要加前缀。
最后一个观点我不太赞同,就是多个第三方库和自己的程序都引入了同一个库,作者觉得要给自己的库加前缀,我觉得没有必要,一是麻烦,加前缀就很麻烦,如果库更新了,每次更新库的时候又要修改一次,引入的库又多,这样重复引入也会加大程序包,完全没必要,多个库引用一个公用的就行了,更新也方便。
第16条:提供“全能初始化方法”
我们把这种可为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(designated initializer)
如果创建类实例的方式不止一种,那么这个类就会有多个初始化方法,不过仍然要在其中选定一个作为全能初始化方法,令其他初始化方法都来调用它。只有在全能初始化方法中,才会存储内部数据。这样,当底层数据存储机制改变时,只需要修改此方法的代码就好。
在init方法中调用全能初始化方法来初始化或者在init方法中抛出异常来提示用户必须使用全能初始化方法来初始化。这样的好处是,即使修改了内部数据的结构,那么只需要修改全能初始化方法就可以了
如果子类的全能初始化方法与超类方法的名称不同,那么应该在该方法中调用超类的全能初始化方法,全能初始化方法的调用链一定要维系,并且总应覆写超类的全能初始化方法(覆写的方法里面调用当前类实例的全能初始化方法或者抛出异常提示用户使用当前类的全能初始化方法)。
每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上,实现“initWithCoder:”时也要这样,应该先调用超类的相关方法,然后再执行与本类有关的任务。
第17条:实现description方法
NSLog(@"object = %@",object);
在构建需要打印到日志的字符串时,object对象会收到description消息,该方法所返回的描述信息将取代“格式字符串”(format string)里的“%@”。
如果在自定义的类上这么做,则只会输出类型和地址信息。这是NSObject类实现的默认方法。此方法定义在NSObject协议里面。
想要输出更多信息,我们就得覆写description方法。覆写的时候也应该打印出类的名字和指针地址。
NSObject协议中还有个方法debugDescription,它是开发者在调试器中以控制台命令打印对象时调用的。在NSObject类的默认实现中,它只是直接调用了description。
第18条:尽量使用不可变对象
尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。
类中可以以_firstName的形式设置只读属性,也不能用点方法设置,除非在类扩展中声明为可读可写的,如下
@property(nonatomic,copy,readwrite) NSString *firstName;
像这种想封装在对象内部的数据,却不想这些数据被外人所改动,那么就在公共接口文件中声明为readonly,在类扩展中重新声明为readwrite。在公共接口中声明的属性可以在扩展中重新声明,属性的其他特质必须保持不变,而readonly可扩展为readwrite。
其实在对象外部,仍然能通过“键值编码”(Key-Value Coding,KVC)技术设置这些属性值。这里就是说如果在类头文件中如果声明属性为只读的,那么不管有没有在类扩展中声明属性为可读可写,都能通过KVC去设置值。但是如果你那样做,后果请自负。
对于集合类的属性,可以参考以下实现。提供一个只读的不可变的属性供外界使用,
@interface EOCPerson : NSObject @property (nonatomic, copy, readonly) NSString *firstName; @property (nonatomic, copy, readonly) NSString *lastName; @property (nonatomic, strong, readonly) NSSet *friends; - (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName; - (void)addFriend:(EOCPerson*)person; - (void)removeFriend:(EOCPerson*)person; @end @interface EOCPerson() @property (nonatomic, copy, readwrite) NSString *firstName; @property (nonatomic, copy, readwrite) NSString *lastName; @end @implementation EOCPerson { NSMutableSet *_internalFriends; } - (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName { if ((self = [super init])) { _firstName = [firstName copy]; _lastName = [lastName copy]; _internalFriends = [NSMutableSet new]; } return self; } - (void)addFriend:(EOCPerson*)person { [_internalFriends addObject:person]; } - (void)removeFriend:(EOCPerson*)person { [_internalFriends removeObject:person]; } - (NSSet*)friends { return [_internalFriends copy]; } @end
这里如果直接使用NSMutableSet来实现friend,可能引发的问题是:当你将这个属性赋值给另外一个指针,例如另外一个对象的一个属性,那么有可能你会修改这个变量,不管添加或者删除,这就直接影响到了friend原来所属的实例,然而,原实例并不知情
第19条:使用清晰而协调的命名方式
参考《苹果 Cocoa 编码规范》和《Google Objective-C Sytle Guide》
第20条:为私有方法名加前缀
一个类所做的事情通常都要比从外面看到的更多,编写类的实现代码时,经常要写一些只在内部使用的方法。应该为这种方法的名称加上某些前缀,这有助于调试。
可以用p_作为前缀,后面使用正常的驼峰法来命名,首字母小写,如p_methodName.
OC语言没办法将方法标为私有,这是运行时决定的,开发者只能在方法命名中体现“私有”。
苹果公司使用一个下划线作私有方法的前缀,并在文档中说明,开发者不应该单用一个下划线做前缀,如果你违背了,很可能会出现问题,比如你继承了一个类,并覆写了父类的同名方法。
你继承的类来自第三方框架的时候,你不知道其私有方法的前缀是什么,此时可以把自己一贯使用的类名前缀用作子类私有方法的前缀,这样能有效避免重名问题。
第21条:理解Objective-C错误模型
ARC在默认情况下不是“异常安全的”(exception safe).也就是如果抛出异常,那么本应该在作用域末尾释放的对象现在却不自动释放了,如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptions。即使不使用ARC,也很难写出在抛出异常时不会导致内存泄漏的代码。
而现在OC采用的步伐是:只在极其罕见的情况下抛出异常,异常抛出后无须考虑恢复问题,而且应用程序此时也应该退出。也就是说,不用再编写复杂的“异常安全”代码了。
异常只用于处理致命错误,那么其他错误怎么办:返回nil/0,或使用NSError。
具体如下:
@interface NSError : NSObject <NSCopying, NSSecureCoding> { @private void *_reserved; NSInteger _code; NSString *_domain; NSDictionary *_userInfo; }
可以通过委托来传递错误,例如NSURLConnection在其委托协议NSURLConnectionDelegate中定义的如下方法:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
还可以通过输入输出参数的形式返回给调用者:
- (BOOL)doSomething:(NSError**)error
用法如下:
NSError *error = nil;BOOL ret = [object doSomething:&error];if (error) { // There was an error}
实际上,在使用ARC时,编译器会把方法签名中的NSError**转换成NSError*__autoreleasing*,(参考《关于__autoreleasing,你真的懂了吗?》)也就是说指针所指的对象会在方法执行完毕后自动释放。
- (BOOL)doSomething:(NSError**)error { // Do something that may cause an error if ( /* there was an error */ ) { if (error) { // Pass the 'error' through the out-parameter *error = [NSError errorWithDomain:domain code:code userInfo:userInfo]; } return NO; ///< Indicate failure } else { return YES; ///< Indicate success } }
这里注意必须先保证error参数不是nil,因为空指针解引用会导致”段错误“(segmentation fault)并使应用程序崩溃,调用者在不关心具体错误时,会给error参数传入nil,所以必须判断这种情况。
NSError的_domain应该定义成NSString型的全局常量,而错误码则定义成枚举类型为佳。
// EOCErrors.h
extern NSString *const EOCErrorDomain;
typedef NS_ENUM(NSUInteger, EOCError) {
EOCErrorUnknown = –1,
EOCErrorInternalInconsistency = 100,
EOCErrorGeneralFault = 105,
EOCErrorBadInput = 500,};
// EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain";
第22条:理解NSCopying协议
NSCopying协议只有一个方法
- (id)copyWithZone:(NSZone*)zone
这个NSZone是以前开发程序时,会据此把内存分成不同的”区“(zone),而对象会创建在某个区里面,现在不用了,每个程序只有一个区:”默认区“(default zone)。所以说,尽管必须实现这个方法,但是你不必担心其中的zone参数。
在可变的类中覆写”copyWithZone“方法时,不要返回可变的拷贝,而应该返回一份不可变的版本。无论当前实例是否可变,若需获取其可变版本的拷贝,均应调用mutableCopy方法。同理,若需要不可变的拷贝,则总应通过copy方法来获取。如下:
-[NSMutableArray copy] => NSArray
-[NSArray mutableCopy] => NSMutableArray
深拷贝与浅拷贝参考《iOS深复制和浅复制》