[编写高质量iOS代码的52个有效方法](五)接口与API设计(下)
先睹为快
19.使用清晰而协调的命名方式
20.为私有方法名加前缀
21.理解Objective-C错误模型
22.理解NSCopying协议
第19条:使用清晰而协调的命名方式
类、方法及变量的命名是Objective-C编程的重要环节。其语法结构使得代码读起来和句子一样。名称中一般都带有in、for、with等介词,而其他编程语言则很少使用这些它们认为多余的字眼:
// C++ string text = "The quick brown fox jumped over the lazy dog"; string newText = text.replace("fox","cat"); // Objective-C NSString *text = @"The quick brown fox jumped over the lazy dog"; NSString *newText = [text stringByReplacingOccurrencesOfString:@"fox" withString:@"cat"];
text.replace(“fox”,”cat”)从字面上无法判断是用fox替换cat还是用cat替换fox,而Objective-C的命名方式虽然长一点,但却非常清晰。
C++或Java更习惯简省的函数名,在这种命名方式下,若想知道每个参数的用途,就得查看函数原型,这会令代码难于读懂,以一个矩形的类为例:
// C++ class Rectangle{ public: Rectangle(float width, float height); float getWidth(); float getHeight(); private: float width; float height; }; // Objective-C @interface EOCRectangle : NSObject @property (nonatomic, assign, readonly) float width; @property (nonatomic, assign, readonly) float height; - (id)initWithWidth:(float)width andHeight:(float)height; @end
当创建该类实例时:
// C++ Rectangle aRectangle = new Rectangle(5.0f,10.0f); // Objective-C Rectangle *aRectangle = [[Rectangle alloc]initWithWidth:5.0f andHeight:10.0f];
C++代码无法判断5.0f和10.0f分别表示什么,就算能推测出来表示矩形的尺寸,也不知道宽度在先还是高度在先,需要查函数定义才能确定。而Objective-C的代码却很清晰。
虽说长名字可伶代码更为易读,但也不能长得太过分了,应尽量言简意赅:
// 好的方法名 - (EOCRectangle*)unionRectangle:(EOCRectangle*)rectangle - (float)area // 不好的方法名 - (EOCRectangle*)union:(EOCRectangle*)rectangle // 不清晰 - (float)calculateTheArea // 太冗余
给方法命名时的注意事项可以总结为:
如果方法的返回值是新创建的,那么方法名的首个词应是返回值类型,除非前面还有修饰语。属性的存取方式不遵循这种命名方式。 应该把表示参数类型的名词放在参数前面。 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。 不要使用str这种简称,应该使用string这样的全称。 返回Bool值的方法应加上has或is前缀。 get这个前缀留给那些借由输出参数来保存返回值的方法。第20条:为私有方法名加前缀
Objective-C语言没办法将方法标为私有,每个对象都可以响应任意消息。需要开发者在命名惯例中体现私有方法等语义。笔者喜欢用p_作为前缀,p表示private,而下划线可以把这个字母和真正的方法名区隔开。
#import @interface EOCObject : NSObject - (void)publicMethod; @end @implementation EOCObject // 公有方法 - (void)publicMethod{ // code } // 私有方法 - (void)p_privateMethod{ // code } @end
苹果公司喜欢单用一个下划线作私有方法的前缀,所以请不要照苹果公司的办法来做,不然有可能无意间重写父类的同名方法。
#import @interface EOCViewController : UIViewController @end @implementation EOCViewController - (void)_resetViewController{ // code } @end
以上代码看起来没有问题,但UIViewController类本身已经实现了一个名叫_resetViewController的方法。这样写的话所有调用都将执行子类中的这个方法。由于超类中的同名方法并未对外公布,除非深入研究这个库,否则根本不会察觉无意间重写了这个方法。
第21条:理解Objective-C错误模型
很多编程语言都有异常(exception)机制,Objective-C也不例外,但需要注意的是,ARC在默认情况下不是异常安全的。这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。想要生成异常安全的代码,需要打开编译器标志-fobjc-arc-exceptions。
Objecti-C现在所采用的方法是:只在极其罕见的情况下抛出异常,异常抛出之后,无需考虑恢复问题,应用程序直接退出。异常只用于处理严重错误,出现一般错误时,令方法返回nil/0,或使用NSError。
比如初始化无法根据传入的参数来初始化当前实例,那么就令其返回nil/0。
- (id)initWithValue:(id)value{ if((self = [super init])){ if(/* 无法用value初始化实例 */){ self = nil; }else{ // 初始化 } } return self; }
而NSError的用法更加灵活,可以把导致错误的原因回报给调用者。NSError对象里封装了三条信息:Error domain(错误范围,类型为字符串)、Error code(错误码,类型为整数)、User info(用户信息,类型为字典)。
// EOCErrors.h #import // 声明全局变量错误范围 extern NSString *const EOCErrorDomain; // 错误码 typedef NS_ENUM(NSUInteger, EOCError){ EOCErrorUnkown = -1, EOCErrorInteralInconsistency = 100, EOCErrorGeneralFault = 105, EOCErrorBadInput = 500, }; @interface EOCErrors : NSObject - (BOOL)doSomething:(NSError**)error; @end // EOCErrors.m #import "EOCErrors.h" NSString *const EOCErrorDomain = @"EOCErrorDomain"; @implementation EOCErrors - (BOOL)doSomething:(NSError *__autoreleasing *)error{ if(/* error发生 */){ if (error) { *error = [NSError errorWithDomain:EOCErrorDomain code:EOCErrorGeneralFault userInfo:@{@"EOCErrorUserInfo":@"General fault occurs!"}]; } return NO; } return YES; } @end
这样就能经由输出参数把NSError对象回传给调用者:
NSError *error = nil; BOOL ret = [object doSomething:&error]; if (error){ // code }
第22条:理解NSCopying协议
使用对象时,经常需要拷贝它。在Objective-C中,此操作通过copy方法完成。如果想令自己的类支持拷贝操作,那就要实现NSCopying协议,该协议只有一个方法:
- (id)copyWithZone:(NSZone*)zone
现在每个程序只有一个区了(默认区),所以实现这个方法时不用担心其中的zone参数。copy方法由NSObject实现,该方法只是以默认区为参数来调用copyWithZone:方法。所以我们想的是重写copy方法,真正需要实现的是copyWithZone:方法。
下面是一个实现NSCopying协议的例子:
// EOCPerson.h #import @interface EOCPerson : NSObject @property (nonatomic, copy, readonly) NSString *firstName; @property (nonatomic, copy, readonly) NSString *lastName; - (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName; - (void)addFriend:(EOCPerson*)person; - (void)removeFriend:(EOCPerson*)person; @end // EOCPerson.m #import "EOCPerson.h" @interface EOCPerson() @property (nonatomic, copy, readwrite) NSString *firstName; @property (nonatomic, copy, readwrite) NSString *lastName; @end @implementation EOCPerson{ NSMutableSet *_friends; } - (void)addFriend:(EOCPerson *)person{ [_friends addObject:person]; } - (void)removeFriend:(EOCPerson *)person{ [_friends removeObject:person]; } - (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{ if ((self = [super init])) { _firstName = [firstName copy]; _lastName = [lastName copy]; _friends = [NSMutableSet new]; } return self; } // 实现NSCopying协议 - (id)copyWithZone:(NSZone *)zone{ // 以全能初始化方法初始化拷贝对象 EOCPerson *copy = [[[self class]allocWithZone:zone]initWithFirstName:_firstName lastName:_lastName]; // 将实例变量拷贝到拷贝对象 copy->_friends = [_friends mutableCopy]; return copy; } @end
本例中,_friends是使用mutableCopy方法来复制的,此方法来自另一个叫做NSMutableCopying的协议,与NSCopying类似,也只定义了一个方法:
- (id)mutableCopyWithZone:(NSZone*)zone
如果需要返回不可变的拷贝,则应该实现NSCopying协议,而若需要返回可变的拷贝,则应实现NSMutableCopying协议:
[NSMutableArray copy] // 返回NSArray [NSArray mutableCopy] // 返回NSMutableArray
编写拷贝方法时,还要决定一个问题,就是应该执行深拷贝还是浅拷贝。深拷贝的意思就是:在拷贝对象自身时,将底层数据也一并复制过去。而浅拷贝只拷贝容器本身,而不复制其中数据。Foundation框架中的所有容器在默认情况下都执行浅拷贝。
在EOCPerson那个例子中,若需要深拷贝的话,可以编写一个专供深拷贝的方法:
- (id)deepCopy{ EOCPerson *copy = [[[self class]alloc]initWithFirstName:_firstName lastName:_lastName]; // copyItem参数设为YES,则会向容器中每个元素都发送copy消息,用拷贝好的元素创建新set返回给调用者。 copy->_friends = [[NSMutableSet alloc]initWithSet:_friends copyItem:YES]; return copy; }