接口与API设计
十五、用前缀避免命名空间冲突
Objective-C没有其他语言那种内置的命名空间(name space)机制。鉴于此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。应用程序的链接过程会出错,类似如下:
duplicate symbol _OBJC_METACLASS_$_EOCTheClass in:
build/something.o
build/something_else.o
duplite symbol_OBJC_$_EOCTheClass in :
build/something.o
build/something_else.o
唯一避免此问题的唯一办法就是变相实现命名空间:为所有名称加上适当前缀
- Apple宣称其保留使用所有"两字母前缀"的权利,所有我们选用的前缀应该是三个字母的。选择与公司名、应用程序相关的前缀。
- 我们总是应该给纯C函数和全局变量加上前缀。
- 若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀
十六、提供"全能初始化方法"
可为对象提供必要信息以便其能完成工作的初始化方法叫做"全能初始化方法",也叫指定初始化方法。
-
在类中提供一个全能初始化方法,并于文档中指明。其他初始化方法,都应调用此全能初始化方法!
-
若子类的全能初始化方法与超类不同,则需覆写超类中的对应方法。如矩形类和正方形类的全能初始化方法不同
-initWithWidth:(float)width andHeight:(float)height; //矩形的全能初始化方法 -initWithDimension:(float)dimension;//正方形的全能初始化方法 在正方形类中,需要覆写掉矩形父类的全能初始化方法,因为该方法不适用于正方形类。
-
如果超类的初始化方法不适用于子类,那么应该覆写这个方法,并在其中抛出异常
-
有时候需要编写多个全能初始化方法。如果某对象的实例有两种完全不同的创建方式,必须分开处理,那么就会出现这种情况。如UI布局中使用nib文件的类。
-(id)initWithCoder:(NSCoder *)decoder;
十七、实现description方法
若调试时需要打印出自定义类的全部属性,需要在自定义的类上覆写description方法,也应该像默认的实现那样打印出类的名字和指针地址。 如下代码:
-(NSString *)description{
return [NSString stringWithFormat:@"<%@: %p,%@>",
[self class],
self,
@{@"title":_title,
@"latitude":@(_latitude),
@"longitude":@(_longitude)}
];
}
如上代码中:用NSDictionary来实现此功能可以令代码更易维护;如果以后还要向类中新增属性,并且要在description方法中打印,那么只需要修改字典内容即可。
NSObject协议中还有一个方法需要注意,那就是debugDescription。此方法是开发者在调试器中以控制台命令打印对象时才调用的。 使用lldb的po命令, 如 po anObject,就会调用debugDescription方法。
要点:
- 实现description方法,返回一个有意义的字符串,用以描述该实例
- 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法
十八、尽量使用不可变对象
编程实践中,应该尽量把对外公布出来的属性设为只读,而且只在确有必要是才将属性对外公布。
- 尽量创建不可变对象
- 若某属性仅可于对象内部修改,则在"class-continuation分类"中将其由readonly属性扩展为readwrite属性。
- 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。
十九、使用清晰而协调的命名方式
Objecti-C中,方法和变量名使用驼峰式大小写命名法。
方法命名:
- 如果方法的返回值是新创建的,那么方法名的首个词应是返回值得类型,除非签名还有修饰词,如localizedString。 属性的存取方法则不遵循这种命名方式。
- 应该把表示参数类型的名称放在参数前面
- 如果方法要在当前对象上执行操作,那么就该包含动词,若执行操作时还需要参数,则应该在动词后面加上一个或多个名词
- 不要使用str这种简称,应该用string这样的全称
- Boolean属性应该加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功能,选用has或is当前缀
- 将get这个前缀留给哪些借由"输出参数"来保存返回值的方法,比如说,把返回值填充到"C语言式数组"里的那种方法就可以使用get做前缀
类与协议的命名
- 命名方式应该协调一致,如果从其他框架中继承子类,那么务必遵循其命名惯例
- 若创建自定义的委托协议,则其名称应该包含委托发起方的名称,后面再跟上Delegate一词
二十、为私有方法名加前缀
- 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开
- 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。一般可用 "p_" 前缀 或者 "类名_"作为前缀,来有效避免重名的问题
二十一、理解Objective-C错误模型
Objective-C语言现在采用的办法是:
- 在极其罕见的情况下抛出异常,异常抛出之后,无需考虑恢复问题,而且应用程序此时也应该退出。
- 在出现非致命错误时,Objective-C语言所用的编程范式为:令方法返回nil/0,或是使用NSError,以表明其中有错误发生
NSError的用法更加灵活,因为经由此对象,我们可以把导致错误的原因汇报给调用者。NSError对象里封装了三条信息:
- Error domain(错误范围,其类型为字符串)
- Error code(错误码,其类型为整数)
- User info(用户消息,其类型为字典)
在设计API时
NSError的第一种常见用法是 通过委托协议来传递错误。
有错误发生时,当前对象会把错误信息经由协议中的某个方法传递给其委托对象(delegate)。 如下:
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
NSError的另一种常见方法是:经由方法的"输出参数"返回给调用者。如下:
-(BOOL)doSomething:(NSError **)error;
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if(error){
//There was an error
}
实际上,在使用ARC时,编译器会把方法签名中的NSError**转换成NSError *__autoreleasing*,也就是说,指针所指的对象会在方法执行完毕后自动释放。
//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";
最好能为自己的程序库中所发生的错误指定一个专用的"错误范围"字符串,使用此字符串创建NSError对象,并将其返回给库的使用者。使用枚举类型来表示错误码。
二十二、理解NSCopying协议
NSCopying协议只有一个方法:
-(id)copyWithZone:(NSZone *)zone;
以前开发程序时,会根据NSZone把内存分成不同的"区"(Zone),而对象会创建在某个区里面。现在不用了,每个程序只有一个区,"默认区"。所以尽管必须实现这个方法,但可以不必担心其中的zone参数了
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCPerson : NSObject<NSCopying>
@property(nonatomic,copy,readonly) NSString *firstName;
@property(nonatomic,copy,readonly) NSString *lastName;
-(id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;
-(void)addFriend:(EOCPerson *)person;
-(void)removeFriend:(EOCPerson *)person;
@end
NS_ASSUME_NONNULL_END
实现
#import "EOCPerson.h"
@implementation EOCPerson{
NSMutableSet *_friends;
}
-(id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName{
if(self = [super init]){
_firstName = [firstName copy];
_lastName = [lastName copy];
_friends = [NSMutableSet new];
}
return self;
}
-(void)addFriend:(EOCPerson *)person{
[_friends addObject:person];
}
-(void)removeFriend:(EOCPerson *)person{
[_friends removeObject:person];
}
-(id)copyWithZone:(NSZone *)zone{
EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName andLastName:_lastName];
copy->_friends = [_friends mutableCopy];
//使用->语法,因为friends并非属性,只是个内部使用的实例变量
return copy;
}
@end
上面代码中,[_friends mutableCopy];是因为NSMutableSet实现了NSMuatbleCopying协议。它也只有一个方法、
-(id)mutableCopyWithZone:(NSZone*)zone;
对于不可变的NSArray和可变的NSMutableArray来说,下列关系总是成立的:
-[NSMutableArray copy] ==> NSArray
-[NSArray mutableArray] ==> NSMutableArray
要点:
- 若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议
- 如虹自定义的对象可分为可变版本与不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议
- 赋值对象是需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝
- 如果你所写的对象需要深拷贝,那么可以考虑新增一个专门执行深拷贝的方法