[编写高质量iOS代码的52个有效方法](五)接口与API设计(下)
参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway
先睹为快
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 <Foundation/Foundation.h>
@interface EOCObject : NSObject
- (void)publicMethod;
@end
@implementation EOCObject
// 公有方法
- (void)publicMethod{
// code
}
// 私有方法
- (void)p_privateMethod{
// code
}
@end
苹果公司喜欢单用一个下划线作私有方法的前缀,所以请不要照苹果公司的办法来做,不然有可能无意间重写父类的同名方法。
#import <UIKit/UIKit.h>
@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 <Foundation/Foundation.h>
// 声明全局变量错误范围
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 <Foundation/Foundation.h>
@interface EOCPerson : NSObject<NSCopying>
@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;
}