第三章:接口与API设计
在开发应用程序的时候,总是不可避免的会用到他人的代码,或者自己的代码被他人所利用,所以要把代码写的更清晰一点,方便其他开发者能够迅速而方便地将其集成到他们的项目里。
第15条:用前缀避免命名空间冲突
-
Objective-C没有内置的命名空间机制(namespace),所以命名的时候需要设法避免潜在的命名冲突,否则就很容易重名了。如果发生命名冲突,那么应用程序的链接过程就会出错。比无法链接更糟糕的是在运行期载入了含有重名类的程序库,这时候就会有"重名符号错误",很可能导致整个程序崩溃。
-
为了避免这种问题,我们可以变相实现命名空间:为所有的名称加上这个程序相关的前缀。需要注意的是,Apple宣称保留使用所有“两字母前缀“的权利,所以自己选用的前缀应该是3个字母。
-
如果在第三方库的基础上进行了开发后再次封包,则需要将所有的文件前缀修改成和第三方库之前匹配的前缀,才可以封包给其他人使用。
example:EOCLibrary里扩展了XYZLibrary的东西,那么就要把XYZLibrary的相关前缀改成EOC的前缀。
第16条:提供“全能初始化方法”
- 所有对象均要初始化,可为对象提供必要信息以便其能完成工作的初始化方法叫做全能初始化方法。如果创建类实例的方法不止一种,那么这个类就会有多个初始化方法,这里需要选定一个初始化方法作为全能初始化方法,令其他初始化方法都调用它。
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
在上面的代码里,initWithTimeIntervalSinceNow
就是全能初始化方法,其他所有的初始化方法都要调用它,所以只有该方法才会存储内部数据,当内部数据改变的时候,仅需要改变全能初始化方法即可。
- 如果子类的全能初始化方法与超类的不同,那么应该覆写超类的全能初始化方法。
- 在Objecive-C程序中,只有发生严重错误的时候才应该抛出异常,初始化的时候抛出异常是不得已之举,表明实例真的没办法初始化了。
第17条:实现description方法
-
调试程序的时候,经常需要打印并查看对象信息,一般有两种方法:
- 编写代码把对象的全部属性输出到日志中。
- 直接打印对象,通过实现description方法,将信息打印出来。
//EOCPerson.h @interface EOCPerson : NSObject @property(nonatomic, copy) NSString *firstName; @property(nonatomic, copy) NSString *lastName; @end //EOCPerson.m - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p,\"%@ %@\">", [self class], self, _firstName, _lastName]; } //test EOCPerson *person = [EOCPerson new]; person.firstName = @"Bob"; person.lastName = @"Smith"; NSLog(@"person = %@", person); /** person = <EOCPerson: 0x7bf240c030f0,"Bob Smith"> **/
-
如果要输出很多互不相同的信息,可以借助
NSDictionary
:
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p,%@>",
[self class],
self,
@{@"firstName":_firstName,
@"lastName":_lastName}
];
}
- 如果想在LLDB里通过
po
指令输出,那么就应该实现debugDescription
方法,和Description
方法一样。
第18条:尽量使用不可变对象
- 在实际编程中,应该尽量把对外公布的属性设为只读,而且只在确有必要的时候才将属性对外公布。
- 如果使用了
readonly
属性,那么有人试着改变属性值,编译器就会报错。这样只能在对象内部进行修改。 - 有时可能想修改封装在对象外部的属性,却不想令这些数据为外人所动,这种情况下可以利用"class-continuation分类",将
readonly
改为readwrite
。其实是等于在内部使用Extension的方式来实现。在对象外部,也可以通过KVC的方式来设置这些属性。
//EOCPointInterest.h
@interface EOCPointInterest : NSObject
@property (nonatomic, copy, readonly) NSString *identifier;
@end
//EOCPointInterest.m
#import "EOCPointInterest.h"
@interface EOCPointInterest()
@property (nonatomic, copy, readwrite) NSString *identifier;
@end
@implementation EOCPointInterest
...
@end
//outter
[pointOfInterest setValue:@"GDGD" forKey:@"identifier"];
这样可以直接改动identifier
属性,即使没有于公共接口中公布这个方法,它依然可以违规的绕过本类的API。
- 定义一些公共API的时候,可能会涉及到collection,这些属性应该设置成可变还是不可变是需要好好考虑清楚的。如果是可变的collection的话,不要把可变的collection作为属性公开,而应该提供相关方法让别人通过方法去修改collection。
第19条:使用清晰而协调的命名方式
- 这一章暂且略过,毕竟命名还是一个比较复杂的部分,而且每个工程都有自己的一套特色。这里只简单说一下大原则。
- 在Objective-C里,命名要求尽量清晰,不使用缩略,让代码读起来像句子一样。
- 类和协议的命名应该加上前缀,避免命名空间冲突。
第20条:为私有方法名加前缀
- 一般来说私有方法是指在内部使用不暴露在外面的方法,这种方法可以通过加前缀的方式方便调试,同时也可以通过前缀来查找以方便统一修改私有代码。
- 不要单用一个下划线作为私有方法的前缀,这是预留给苹果公司用的。
第21条:理解Objective-C错误模型
- 在Objective-C里,自动引用计数默认情况下不是异常安全的,也就是如果抛出异常那么该对象就不会自动释放了。如果要异常安全的话需要做一些额外操作。
- 即使使用ARC,也可能导致内存泄露:
id someResource = [someClass new];
if (/* check for error */) {
@throw [NSEXception exceptionWithName:@"Exception"
reason:@"ther is a error"
userInfo:nil];
}
[someResource doSomething];
[someResource release];
如果上述代码发生了异常之后,那么资源就不可能被释放掉了。如果要正确释放应该在抛出异常之前释放掉资源。
-
现在Objective-C采用的方法是尽量不抛出异常,如果抛出异常了无需考虑恢复问题,应用程序也应该退出。
-
如果不是那么严重的异常,一般可以令方法返回
nil/0
,或者是使用NSError
,表明有错误发生。 -
NSError
的用法更加灵活,并且这个模型可以添加描述错误的原因。- Error domain (错误范围,类型为字符串):错误发生的根源,通常用一个特有的全局变量来定义。
- Error code (错误码,类型为整数):独有的错误代码,用来指明在某个范围内具体发生了何种错误。
- User info (用户信息,类型为字典):关于错误的额外信息。
NSError
的常见用法主要有两种:- 通过委托协议来传递此错误,有错误发生时,当前对象会把错误信息经由协议中的某个方法给其委托对象。例如:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
- 经由输出参数返回给调用者,例如:
- (BOOL)doSomething:(NSError **)error; //example NSError *error = nil; BOOL ret = [object doSomething:&error]; if (error) { //handle the error }
实际上使用ARC的时候,编译器会把方法签名中的
NSError **
转换成NSError * __autoreleasing*
,也就是指针所指的对象在方法执行完毕后自动释放。 定义错误码的时候最好使用枚举的方式实现,并且最好在定义这些枚举的头文件里对每个错误类型详细说明。
第22条:理解NSCopying协议
- 如果想要自己的类支持copy方法,那就要实现
NSCopying
协议,该协议只有一个方法:
- (id)copyWithZone:(nullable NSZone *)zone;
NSZone
是以前开发程序时,会把内存分成不同的区,而对象会创建在不同的区,现在每个程序只有一个默认区,尽管必须实现这个方法,但是zone
参数不用担心。copy方法是由NSObject
实现,该方法只是以默认区为参数来调用。
example:
//EOCPerson.h
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject<NSCopying>
@property(nonatomic, copy) NSString *firstName;
@property(nonatomic, copy) NSString *lastName;
@end
//实现NSCopying的方法
- (id)copyWithZone:(NSZone *)zone {
EOCPerson *copy = [[self class] allocWithZone:zone];
copy.firstName = _firstName;
copy.lastName = _lastName;
return copy;
}
- 如果要访问类的内部实例变量(并非属性),那么需要用
->
的语法来访问。
@implementation EOCPerson {
NSMutableSet *_friends;
}
//如果要访问,那么就是
- (id)copyWithZone:(NSZone *)zone {
/* copy something*/
copy->_friends;
}
- 除了copy方法以外,还有mutableCopy方法,这个是来自于
NSMutableCopying
的协议,如果要实现一个可变的拷贝,那么就需要实现该协议的方法:
- (id)mutableCopyWithZone:(nullable NSZone *)zone;
对于不可变的NSArray与可变的NSMutableArray来说,下列关系成立:
[NSArray copy] => NSArray;
[NSArray mutableCopy] => NSMutableArray;
[NSMutableArray copy] => NSArray;
[NSMutableArray mutableCopy] => NSMutableArray;
-
在编写拷贝方法的时候,还需要注意一个问题那就是拷贝执行的是深拷贝还是浅拷贝。
- 深拷贝:拷贝对象的时候底层数据也一并复制过去。
- 浅拷贝:只拷贝容器本身,而不复制其中数据。
Foundation
框架的所有collection
类在默认情况下都执行浅拷贝,因为collection
里可能存在无法拷贝的数据,或者并非需要深拷贝。
- (id)deepCopy { EOCPerson *copy = [[[self class] alloc] init]; copy.firstName = _firstName; copy.lastName = _lastName; return copy; }
网友评论