接口与API设计

我们在构建应用程序是,可能想将其中部分代码用于后续项目,也可能想把某些代码发布出来,供他人使用。如果决定重用,那我们在编写接口时就会将其设计成易于复用的形式。这需要用到OC中常见的编程范式,同时还需了解各种可能碰到的陷阱。
为了方便别人使用你的代码,要把代码写的清晰一点,以便其他开发者能够迅速而方便地将其集成到他们的项目里。

一、用前缀避免命名空间冲突

OC没有其他语言那种内置的命名空间机制。因此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。
如果出现命名冲突,会导致该类所对应的类符号和“元类”符号各定义了两次。而且比无法链接更糟糕的清况是,在运行期载入了含有重名类的程序库。此时,“动态加载器”就遭遇了“重名符号错误”,很可能会令整个应用程序崩溃。
避免此问题的唯一办法就是变相实现命名空间:为所有名称都加上适当前缀。所选前缀可以是与公司、应用程序或二者皆有关联之名。即使加了前缀也可能出现命名冲突,但是器几率会小很多。
使用Cocoa创建应用程序时一定要注意,Apple宣称其保留使用所有“两字母前缀”的权利,所以你自己选用的前缀应该是三个字母的。
不仅是类名,应用程序中的所有名称都应加前缀。如果要为既有类新增“分类”,那么一定要给“分类”及“分类”中的方法加上前缀,开发者可能会忽视另外一个容易引起命名冲突的地方,那就是类的实现文件中所用的纯C函数及全局变量,在编译好的目标文件中,这些名称是要算做“顶级符号”的。
例如:
某个事件函数中已经自动创建了一个名叫completion的函数,然后开发者又手动创建了一个名叫completion的函数,那么就会于链接时发生类似下面这种“重复符号错误”:

duplicate symbol _completion in:
	build/EOCSoundPlayer.o
	build/EOCAnotherClass.o

如果将代码发布为程序库,供他人在开发应用程序时使用,那么就更糟糕了。这等于办了一件坏事:因为已经有了名为_completion的符号,所以使用此程序库的开发者就无法再创建名为completion的函数了。
由此可见,我们总时应该给这种C函数的名字加上前缀。刚才的completion函数就可以改名为EOCSoundPlayerCompletion这么做还有个好处:若此符号出现在栈回溯信息中,则很容易就能判明问题源自哪块代码。
如果用第三方库编写自己的代码,并准备将其在发布为程序库供他人开发应用程序所用,那么尤其要注意重复符号问题。在这里插入图片描述
如果应用程序自身和其所用的程序库都引入了同名的第三方库,则后者应加前缀一避免命名冲突。
如上图所示:
例如你准备发布的程序库叫EOCLibrary,其中引入了名为XYZLibrary中的所有名字都冠以EOC。于是,应用程序就可以随意使用它自己直接引入的那个XYZLibrary 库了,而不用担心与EOCLibrary里的这个XYZLibrary相冲突。
要点:

  1. 选择与你的公司、应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀。
  2. 若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀。

二、提供“全能初始化方法”

所有对象均要初始化。初始化时,有些对象可能无须开发者向其提供额外信息,不过一般来说还是要提供的。以iOS的UI框架UIKit为例,其中有个类叫做UITableViewCell,初始化该类对象时,需要指明其样式及标识符,标识符能够区分不同类型的单元格。由于这种对象的创建成本较高,所以绘制表格时可依照标识符来复用,以提升程序效率。我们把这种可为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”。
如果创建类实例的方法不止一种,那么这个类就会有多个初始化方法。这当然很好,不过仍然要在其中选定一个作为全能初始化方法,令其他初始化方法都来调用它。NSDate就是个例子,其初始化方法如下:

- (id)init
- (id)initWithString: (NSString *)string
- (id)initWithTimeIntervalSinceNow: (NSTimeInterval)seconds
- (id)initWithTimeInterval: (NSTimeInterval)seconds sinceDate: (NSDate *)refDate
- (id)initWithTimeIntervalSinceReferenceDate: (NSTimeInterval)seconds
- (id)initWithTimeIntervalSince1970: (NSTimeInterval)seconds

正如该类的文档所描述的那样,在上面几个初始化方法中,“initWithTimeIntervalSinceReferenceDate:”是全能初始化方法。也就是说,其余的初始化方法都要调用它。于是,只有在全能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需修改此方法的代码就好,无须改动其他初始化方法。
比如说,我们要编写一个表示矩形的类。其接口可以这样写:

#import <Foundation/Foundation.h>

@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
@end

我们把属性声明为只读。不过这样一来,外界就无法设置Rectangle对象的属性了。开发者可能会提供初始化方法以设置这两个属性:

- (id)initWithWidth:(float)width andHeight:(float)height {
	if ((self = [super init])) {
		_width = width;
		_height = height;
	}
	return self;
}

可是,如果有人用[[EOCRectangle alloc] init];来创建矩形会如何呢?这么做时合乎规则的,因为EOCRectangle的超类NSObject实现了这个名为init的方法,调用完该方法后,全部实例变量都将设为0(或设置成符合其数据类型且与0等价的值)。如果我们想要用该方法初始化之后的矩形是一个默认的宽度与高度值,或是抛出异常,指明本类实例必须用“全能初始化方法”来初始化。也就是说可以按照下方的其中一种方法覆写init方法:

//Using default values
- (id)init {
	return [self initWithWidth:5.0 fandHeight:10.0f];
}

//Throwing an exception
- (id)init {
	@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:andHeight: instead." userInfo:nil];
}

但是,我们设置默认值的那个init方法调用了全能初始化方法。采用增额版本来覆写,那么也可以直接在其代码中设置_width与_height实例变量的值。然而,若是类的底层存储方式改变了(比如开发者决定把宽度与高度一起放在某结构体中),则init与全能初始化方法设置数据所用的代码就都要修改。但是如果类的初始化方法有多种,而且待初始化的数据也较为复杂,那么这样做就麻烦得多。很容易就忘了修改其中某个初始化方法,从而导致各个初始化方法之间互相不一致。
假如我们现在创建一个EOCSquare类,令其成为EOCRectangle的子类。这种继承方式完全合理,不过,新类的初始化方法应该想如下编写:

#import "EOCRectangle.h"

@interface EOCSquare : EOCRectangle
- (id)initWithDimension:(float)dimension;
@end

@implementation EOCSquare

- (id)initWithDimension:(float)dimension {
	return [super initWithWidth:dimension andHeight:dimension]
}

@end

上述方法就是EOCSquare类的全能初始化方法。请注意,它调用了超类的全能初始化方法。回过头看看EOCRectangle类的实现代码,你就会发现,那个类也调用了其超类的全能初始化方法。全能初始化方法的调用链一定要维系。然而,调用者可能会使用init方法或initWithWidth: andHeight:方法来初始化EOCSquare对象。类的编写者肯定是不愿意这样的事情发生的,因为这样子会创建出宽高不一致的正方形。于是就产生了一个问题,如果子类的全能初始化方法与超类方法的名称不同,那么总应覆写超类的全能初始化方法,在上方的例子中就应该像下面这样覆写EOCRectangle的全能初始化方法:

- (id) initWithWidth: (float)width andHeight: (float)height {
	float dimension = MAX(width, height);
	return [self initWithDimension:dimension];
}

覆写了这个方法后,即便使用init来初始化EOCSquare对象,也能照常工作。原因在于,EOCRectangle类覆写了init方法,并以默认值为参数调用了该类的全能初始化方法。
有时候我们不想覆写超类的全能初始化方法,因为那样做没有道理,现在不想令initWithWidth:andHeight:方法以其两参数中较大者作边长来初始化EOCSquare对象。反之,我们认为这是方法调用者自己犯了错误。这种情况下,常用的办法是覆写超类的全能初始化方法并于其中抛出异常:

- (id) initWithWidth:(float)width andHeight:(float)height {
	@throw [NSException exceptionWithName:NSInternalInconsistancyException reason:@"Must use initWithDimension: instead." userInfo:nil];
}

这样做看起来似乎显得突兀,不过有时却是必需的,因为那种情况下创建出来的对象,其内部数据有可能相互不一致。这时候在EOCRectangle与EOCSquare这个例子中,调用init方法也会抛出异常,因为init方法也得调用“initWithWidth:andHeight:”。此时可以覆写init方法,并在其中以合理的默认值来调用initWithDimension:方法:

- (id) init {
	return [self initWithDimension:5.0f];
}

不过,在OC中,只有发生严重错误时才应该抛出异常,所以,初始化方法抛出异常乃是不得已之举,表明实例真的没办法初始化了 。
有时候可能需要编写多个全能初始化方法。比如说:如果某对象的实例有两种完全不同的创建方法,必须分开处理,那么就会出现这种情况。以NSCoding 协议为例,此协议提供了“序列化机制”,对象可依此指明其自身的编码及解码方式。NSCoding协议定义了下面这个初始化方法,遵从该协议者都应实现此方法:

- (id) initWithCoder:(NSCoder *)decoder;

我们在实现此方法时一般不调用平常所使用的那个全能初始化方法,因为该方法要通过“解码器”将对象数据解压缩,所以和普通的初始化方法不同。而且,如果超类也实现了NSCoding,那么还需要调用超类的initWithCoder:方法。于是,子类中有不止一个初始化方法调用了超类的初始化方法,因此,严格的说,在这种情况下出现了两个全能初始化方法。
具体到EOCRectangle这个例子上,其代码就是:

#import <Foundation/Foundation.h>

@interface EOCRectangle : NSObject<NSCoding>

@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;

- (id)initWithWidth:(float)width andHeight:(float)height;
@end

@implementation EOCRectangle

//Designated initializer
- (id)initWithWidth:(float)width andHeight:(float)height {
	if ((self = [super init])) {
		_width = width;
		_height = height;
	}
	return self;
}

//Superclass's designated initializer
- (id)init {
	return [self initWithWidth:5.0f andHeight:10.0f];
}

//Initializer from NSCoding
- (id) initWithCoder:(NSCoder *)decoder {
	//Call through to super's designated initializer
	if ((self = [super init])) {
		_width = [decoder decodeFloatForKey:@"width"];
		_height = [decoder decodeFloatForKey:@"height"];
	}
	return self;
}

@end

请注意,NSCoding协议的初始化方法没有调用本类的全能初始化方法,而是调用了超类的相关方法。然而,若超类也实现了NSCoding,则需改为调用超类的initWithCoder.初始化方法。例如,在此情况下,EOCSquare类就得这么写:

#import "EOCRectangle.h"

@interface EOCSquare : EOCRectangle
- (id)initWithDimension:(float)dimension;
@end

@implementation EOCSquare

//Designated initializer
- (id) initWithDimension:(float)dimension {
	return [super initWithWidth:dimension andHeight:dimension];
}

//Superclass designated initializer
- (id) initWithWidth:(float)width andHeight:(float)height {
	float dimension = MAX(width, height);
	return [self initWithDimension:dimension];
}

//NSCoding designated initialzer
- (id) initWithCoder:(NSCoder *)decoder {
	if ((self = [super initWithCoder:decoder])) {
		//EOCSquare's specific initalizer
	}
	return self;
}

@end

每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上,实现initWithCoder:时也要这样,应该先调用超类的相关方法,然后在执行与本类有关的任务。这样编写出来的EOCSquare 类就完全遵守NSCoding协议了。如果编写initWithCoder:方法时没有调用超类的同名方法,而是调用了自制的初始化方法,或是超类的其他初始化方法,那么EOCRectangle类的initWithCoder:方法就没有机会执行,于是,也就无法将_width及_height这两个实例变量解码了。
要点:

  1. 在类汇总给你提供一个全能初始化方法,并与文档里指明。其他初始化方法均应调用此方法。
  2. 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
  3. 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

三、实现descreption方法

调试程序时,经常需要打印并查看对象信息。一种办法是编写代码把对象的全部属性都输出到日志中。不过最常见的做法还是像下面这样:

NSLog(@"object = %@", object);

在构建需要打印到日志的字符串时,object对象会收到description消息,该方法所返回的描述信息将取代“格式字符串”里的%@。比方说,object是个数组,若用下列代码打印其信息:

NSArray *object = @[@"A string", @(123)];
NSLog(@"object = %@", object);

则会输出:

object = {
	"A string"
	123
}

然而,在自定义的类上这么做,那么输出的信息却是下面这样:

object = <EOCPerson: 0x7fd9a1600600>

与object为数组时所输出的信息相比,上面这种内容不太有用。除非在自己的类里覆写description方法,否则打印信息时就会调用NSObject类所实现的默认方法。此方法定义在NSObject协议里,不过NSObject类也实现了它。但是可以看到,这种方法只是打印了类名和对象的内存地址。只有在你想判断两指针是否真的指向同一对象时,这种信息才有用。除此之外,我们肯定想要打印更多的信息。想要实现这个需求,我们只需要覆写description 方法并将描述此对象的字符串返回即可。例如,有下面这个 代表个人信息的类:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;

- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end

@implementation EOCPerson

- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
	if ((self = [super init])) {
		_firstName = [firstName copy];
		_lastName = [lastName copy];
	}
	return self;
}
@end

该类的description方法通常可以这样实现:

- (NSString *) description {
	return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", [self class], self, _firstName, _lastName];
}

如果按照上方的代码的话,那么EOCPerson对像的输出结果就会如下:

EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Bob" lastName:@"Smith"];
NSLog(@"person = %@", person);
//Output
//person = <EOCPerson: 0x7fb249c030f0, "Bob Smith">

这样一来,就比之前光打印地址所输出的信息更加清楚和有用了。还有一个不错的建议,就是在打印出来的信息里头也打印对象和类的地址信息。而且刚才我们能发现,NSArray类的对象默认就没有打印地址信息,所以实现description方法时,没有固定规则可循,应根据当前对象来决定在description方法里打印何种信息。
有个简单的办法,可以在description中输出很多互不相同的信息,那就是借助NSDictioniary类的description方法。此方法输出的信息的格式如下:

{
	key: value;
	foo: bar;
}

在自定义的description方法中,把待打印的信息放到字典里面,然后将字典对象的description方法所输出的内容包含在字符串里并返回,这样就可以实现精简的星系输出方式了。例如:下面这个类表示某地点的名称和地理坐标(纬度与经度):

#import <Foundation/Foundation.h>

@interface EOCLocation : NSObject
@property (nonatomic, copy, readonly) NSString *title;
@property (nonatomic, assign, readonly) float latitude;
@property (nonatomic, assign, readonly) float longitude;
- (id)initWithTitle:(NSString *)title latitude:(float)latitude longitude:(float)longitude;
@end

@imiplementation EOCLocation
- (id)initWithTitle:(NSString *) title latitude:(float) latitude longitude:(float)longitude {
	if ((self = [super init])) {
		_title = [title copy];
		_latitude = latitude;
		_longitude = longitude;
	}
	return self;
}

@end

如果这个类的的description方法能够打印出地名和经纬度就好了。所以我们就可以像下面这样编写description方法,用NSDictionary来实现此功能:

- (NSString *) description {
	return [NSString stringWithFormat:@"<%@: %p, %@>",
			[self class],
			self,
			@{@"latitude":_title,
			@"latitude":@(_latitude),
			@"longitude":@(_longitude)}
		];
} 

输出的信息格式为:

location = <EOCLocation: 0x7f98f2e01d20, {
	latitude = "51.506"
	longitude = 0;
	title = London;
}>

这比仅仅输出指针和类名要有用多了,而且对象中的每条属性都能打印得很好。也可以在格式字符串中为每个实例变量预留好位置,但是,用NSDictionary来实现该功能可以令代码更容易维护:如果以后还要向类中新增属性,并且要在description方法中打印,那么只需要修改字典内容即可。
NSObject协议中还有个方法需要注意,那就是debugDescription,此方法的用意与description非常相似。二者的区别在与,debugDescription方法是开发者在调试器中以控制台命令打印对象时才调用的。在NSObject类的默认实现中,此方法只是直接调用了description。以EOCPerson类为例,我们在创建实例所用的代码后面插入断点,然后通过调试器(假设使用LLDB)运行程序,使之暂停于此:

EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Bob" lastName:@"Smith"];
NSLog(@"person = %@", person);
//Breakpoint here

当程序运行到断点的时候,开发者就可以向调试控制器台里输入命令了。LLDB的“po”命令可以完成对象打印(print- object)工作,其输出如下:

EOCTest[640:c07] person = <EOCPerson: 0x712a4d0, "Bob Smith">
(11db) po person
(EOCPerson *)$1 = 0x0712a4d0 <EOCPerson: 0x712a4d0, "Bob Smith">

控制台中的“(EOCPerson *)$1 = 0x0712a4d0”是由调试器所添加的,器后的内容才是由debugDescription所返回的信息。
如果想要返回更为详细的信息,此时可用下列代码实现这两个方法:

- (NSString *) description {
	return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}

- (NSString *) debugDescription {
	return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", [self class], self, _firstName, _lastName];
}

现在我们在将刚才的代码运行一次,这次po命令所打印出来的对象信息如下所示:

EOCTest[640:c07] person = Bob Smith
(11db) po person
(EOCPerson *)$1 = 0x07110fb0 <EOCPerson: 0x7117fb0, "Bob Smith">

你可能不想把类名与指针地址这种额外内容放在普通的描述信息里,但是却希望调试的时候能够很方便的看到他们,在此情况下,就可以使用这种输出方式来实现。Foundation框架的NSArray类就是这么做的。
要点:

  1. 实现description方法返回一个有意义的字符串,用以描述该实例
  2. 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法。

四、尽量使用不可变对象

默认情况下,属性是“既可读又可写的”,这样创建出来的类都是可变的,但是在设计类的时候,我们应充分运用属性来封装数据。而在使用属性时,则可将器声明为“只读”。
一般情况下,我们要建模的数据未必都需要改变。比如说,我们从服务器中请求来的数据加载成UI显示在屏幕上,这些数据一般都是不需要去改变的,而且,即使我们修改了这些数据,它们也不会传送回服务器中,而且被修改的数据可能会造成数据结构的破坏,使其失去固有的语义。因此,大家尽量要减少对象中的可变内容。
具体到编程中,则应该尽量吧对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。
例如:

我们一开始写出来的代码中的属性可能是这样的:
@property (nonatomic, copy) NSString *identifier;

这个值是从服务器中获取的标识符,所以我们无需对其进行修改,为了让该属性不可变,我们需要把其声明为readonly:

@property (nonatomic, copy, readonly) NSString *identifier;

现在的话,如果有人试着改变属性值,那么编译的时候就会报错。对象汇总给你的属性值可以读出,但是无法写入,这就能保证网络请求来的数据之间都是相互协调的。于是,开发者在使用对象时就能肯定其底层数据不会改变。比如,用这些数据显示出来的UI它们就不会由于数据改变而出错。
有人现在也许会问,既然这些属性都没有设置方法(setter),那为何还要指定内存管理语义呢,如果不指定,采用默认的语义也可以:

@property (nonatomic, readonly) NSString *identifier;

虽说如此,但是我们还是应该在文档里指明实现所用的内存管理语义,这样的话,以后想把它变为可读写的属性时,就会简单一些。
有时候肯想修改封装在对象内部的数据,但是却不想令这些数据被外人所改动。这种情况下,通常的做法就是在对象内部将readonly属性重新声明为readwrite,当然,如果该属性是nonatomic的,闹这样做能会产生“竞争条件”。在对象内部写入某属性时,对象外的观察者也行正在读取该属性。若想避免此问题,我们可以在必要时通过“派发队列”等手段,将(包括对象内部的)所有数据存取操作都设为同步操作。
将属性重新声明为readwrite这一操作可于“分类”中完成,在公共接口中声明的属性可于此处重新声明,属性的其他特质必须保持不变就解决,而readonly可以扩展为readwrite。
例如上方的例子,在其所声明类的分类中,可以写成下方的样子:

@property (nonatomic, copy, readwrite) NSString *identifier;

现在,只能于其属性所在类的实现代码内部设置这些属性了,其实更准确的说,在对象外部,仍然能通过“键值编码”(KVC)技术设置这些属性值,比如说,可以像下面这样修改属性值:

[pointOfInterest setValue:@"abc" forKey:@"identifier"];

这样子可以改动属性值,因为KVC会在类里查找“setIdentifier:”方法,并借此修改此属性。即使没有于公共接口中公布此方法,它也依然包含在类中。不过,这样做等于违规地绕过了本类所提供的API,要是开发者使用这种“杂技代码”的话,那么得自己开应对可能出现的问题。
还有一种可以修改数据的方法就是直接用类型信息查询功能查出属性所对应的实例变量在内存布局中的偏移量,以此来人为设置这个实例变量的值。这样做比绕过本类的公共API还要不合规范。所以不应该因为这个原因而忽视所提的建议,大家还是要尽量编写不可变的对象。
在定义类的公共API时,还要注意一件事情:对象里表示各种collection的那些属性究竟应该设成可变的,还是不可变的。
假设我们可以创建一个容器属性,我们可以提供一个readonly属性供外界使用,该属性将返回不可变的set,而此set则是内部那个可变set的一份拷贝。例如下面代码:

#import <Foundation/Foundation.h>

@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) removeFirend: (EOCPerson *)person;
@end

#import "EOCPerson.h"

@interface EOCPerson()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end

@implementation EOCPerson {
	NSMutableSet *_internalFriends;
}

- (NSSet *)friends {
	return [_internalFriends copy];
}

- (void) addFriend:(EOCPerson *)person {
	[_internalFriends addObject:person];
}

- (void) removeFriend: (EOCPerson *)person {
	[_internalFriends removeObject:person];
}

- (id) initWithFirstName: (NSString *)firstName andLastName:(NSString *) lastName {
	if ((self = [super init])) {
		_firstName = firstName;
		_lastName = lastName;
		_internalFriends = [NSMutableSet new];
	}
	return self;
}
@end

也可以用NSMutableSet来实现friends属性,令该类的用户不借助“addFriend:”与"removeFriend:"方法而直接操作此属性,但是这种过分解藕数据的做法很容易出bug。比方说,在添加或删除朋友时,EOCPerson对象可能还有执行其它操作,若采用这种做法,就等于直接从底层修改了其内部用于存放朋友对象的set。在EOCPerson对象不知情时,直接修改set肯会令对象内的各数据之间互不一致。
另外,不要在返回的对象上查询类型以确定其是否可变。
要点:

  1. 尽量创建不可变的对象
  2. 若某属性进可于对象内部修改,则在“分类”中将其由属性扩展为readwrite属性
  3. 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。

五、使用清晰而协调的命名方式

一开始学习的人通常会觉得OC的语言很繁琐,因为其语法结构使得代码读起来和句子一样。名称中一般都有"in"、“for”、"with"等介词,而其他语言很少使用这些它们认为多余的字眼,以下方代码为例:

NSString *text = @"The quick brown fox jumped over the lazy dog";
NSString *newText = [text stringByReplacingOccurrencesOfString:@"fox" withString:@"cat"];

这个句子虽然繁长,但是准确描述了开发者想做的事。
下面介绍一种命名方式:
驼峰命名法:
方法和变量名的首个单词的首个字母小写,然后后面的每个单词的首字母大写。类名也用驼峰命名法,另外,类名一般还有三个前缀字母。
用一个例子表示OC方法的优点:

C++的方法:
Rectangle *aRectangle = new Rectangle(5.0f, 10.0f);
OC的方法:
- (id) initWithSize:(float)width :(float)height;

很显然,OC的方法非常清晰地就可以看出第一二个变量分别表示什么。
虽然说,使用长名字可令代码更为易读,但是OC新手还是难于习惯这种详尽的方法命名风格。不要吝于使用长方法名,把方法名起得稍微长一点,可以保证其能准确传达方法所执行的任务。然而方法名也不能长的太过分了,应尽量言简意赅。
清晰的方法名从左至右读起来就像一段文章。并不是说非得按照那些命名规则来给方法起名,不过这样做可以时代码变得更好维护,而且也使其他人更易读懂。
NSString 这个类,就展示了一套良好的命名习惯:

· +string工厂方法,用于创建新的空字符串。方法名清晰地描述了返回值的类型
· +stringWithString 工厂方法,根据某字符串创建出与之内容相同的新字符串。与创建空字符串所用的工厂方法一样,方法名的第一个单词也指明了返回类型。
· +localizedStringWithFormat:工厂方法,根据特定格式创建出来的新的“本地化字符串”

给方法命名的时候的注意事项可总结成下面几条规则:

  1. 如果方法的返回值时新长创建的,那么方法名的首个词应时返回值的类型,除非前面还有修饰语,
  2. 应该把表示参数类型的名词放在参数前面
  3. 如果方法要在当前对象上执行操作,那么就应该包含动词,若执行操作时还需要参数,则应该在动词后面加上一个或多个名词
  4. 不要使用str这种简称,应该用string这样的全称
  5. Boolean属性应加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功能,选用has或is当前缀。
  6. 将get这个前缀留给那些借由"输出参数"来保存返回值的方法,比如说,把返回值填充到"C语言式数组"里的那种方法就可以使用这个词做前缀

六、类与协议的命名

应该为类与协议的名称加上前缀,以避免命名空间冲突,而且应该像给方法起名时那样把词句组织好,使其从左至右读起来较为通顺。例如,在NSArray 的子类中,有一个表示可变数组的类叫NSMutableArray, mutable这个词放在array前面用以表示这是一种特殊的array数组。
下面以iOS的UI库UIKit为例,演示类与协议的命名惯例:

· UIView类
所有“视图”均继承与此类。视图是构造用户界面的基本单元,它们负责绘制按钮、文本框、表格等控件。这个类的名字无须解释即可自明其意,开头的两个字母"UI"是UIKit框架的通用前缀。
· UIViewController类
视图类负责绘制视图,然而去不负责指定视图里面应该显示的内容。这项工作由本类,也就是“视图控制器”来完成。其名称从左至右读起来很顺。

其中最重要的一点就是,命名方式有关协调一致。而且,如果要从其他框架中继承子类,那么无比遵循其命名惯例。比方说,要从UIView类中继承自定义的子类,那么类名末尾的词必须是View。同理,若要创建自定义的委托协议,则其名称中应该也包含委托发起方的名称,后面在跟上Delegate一词。如果能坚持这种命名习惯,那么在稍后回顾自己的代码或他人食欲哦给你你所写的代码是,就很容易理解其含义。
要点:

  1. 起名时应遵从标准的OC命名规范,这样创建出来的接口更容易为开发者所理解。
  2. 方法名要言简意赅,从左至右读读起来要像个日常用语中的句子才好
  3. 方法名里不要使用缩略后的类型名称
  4. 给方法起名的第一要务就是确保其分格与你自己的代码或所要集成的框架相符。

七、为私有方法名加前缀

一个类所做的事情通常都要比从外面看到的更多,编写类的实现代码时,经常需要写一些只在内部使用的方法。应该为这种方法的名称加上某些前缀,这有助于调试因为据此很容易能把公共方法和私有方法区别开。
为私有方法名加前缀还有个原因,就是便于修改方法名或方法签名。对于公共方法来说,修改其名称或签名之前要三思,因为类的公共API不便随意改动。那么使用这个类的所有开发者都必须更新其代码才行。二对于内部方法来说,若要修改其名称或签名,则只需同时修改本类内部的相关代码即可,不会影响到面向外界的那些API。用前缀把私有方法标出来,这样很容易就能看出哪些方法可以随意修改,哪些不应该轻易改动。
具体使用何种前缀可根据个人喜好来定其中最好包含下划线与字母p,笔者喜欢用p_作为前缀,p表示“private”(私有的),而下划线则可以把这个字母和真正的方法名区隔开。下划线后面的部分按照常用的驼峰命名即可。其首字母要小写。例如,包含私有方法的EOCObject类可以这样写:

#import <Foundation/Foundation.h>

@interface EOCObject : NSObejct
- (void) publicMethod;
@end

@implementation EOCObject

- (void)publicMethod {
	/* ... */
}

- (void) p_privateMethod {
	/* ... */
}

@end

与公共方法不同,私有方法不出现在接口定义中。有时可能要在“分类”里声明私有方法,然而最近修订的编译器已经不要求在使用方法前必须先行声明了。所以说,私有方法一般只在实现的时候声明。
OC语言没法讲方法标为私有,且OC中没有办法将方法标为私有。OC中没有那种约束方法调用的机制用以限定谁能用此方法、能在哪个对象上调用此方法以及可是能调用此方法。所以OC开发者会在命名惯例中体现出“私有方法”等语义。
苹果公司喜欢单用一个下划线做私有方法的前缀。所以开发者当避免这样使用前缀,否则就可能会惹来大麻烦,因为你有可能会无意间覆写父类的同名方法。
总之,在确定使用了前缀的情况下,如果子类所继承的那个类既不在苹果公司的框架中,也不在你自己的项目中,而是来自别的框架,那么除非该框架在文档中明示,否则你无法知道其私有方法所加的前缀是什么。此时可以把自己一贯使用的类名前缀用作子类私有方法的前缀,这样能有效避免重名问题。同时还应该考虑到其他人会如何从你所写的类中继承子类,这也是私有方法应该加前缀的原因。除非使用一些相当复杂的工具,否则,在没有源代码的情况下,无法知道某个类在其公共接口之外还定义并实现了哪些方法。
要点:

  1. 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
  2. 不要单用一个下划线做私有方法的前缀,我也这种做法是预留给苹果公司的。

八、理解OC错误模型

当前很多种编程语言都有“异常”机制,OC也不例外。
首先要注意“自动引用计数”在默认情况下不是“异常安全的”。具体来说,这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫-fobjc-arc-exceptions。
即使不用ARC, 也很难写出在抛出异常时不会导致内存泄漏的代码。比如说,设有段代码创建好了某个资源,使用完之后在将其释放。可是,在释放资源之前如果抛出异常那么该资源就不会被释放了。
OC语言现在所采用的办法是:只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说,不用再编写复杂的“异常安全”代码了。
异常只应该勇于极其严重的错误,比如说,你编写了某个抽象基类,它的正确用法是先从中继承一个子类,然后使用这个子类。这种情况下,如果有人直接使用了这个抽象类,那么就可以考虑抛出异常,OC没办法将某个类标识称“抽象类”,想要达成这个效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。这样的话,只要有人直接创建抽象基类的实例并使用它,即会抛出异常:

- (void) mustOverrideMethod {
	NSString *reason = [NSString stringWithFormat: @"%@ must be oberridden", NSStringFormSelector(_cmd)];

	@throw [NSException exceptionWithName:NSInterNalInconsistencyException reason:reason userInfo:nil];
}

除了勇于处理严重错误,那么对于其他错误的处理方法就是:OC语言所用的编程范式为:令方法返回nil/0,或者是使用NSError,以表明其中有错误发生。比如说,如果初始化方法无法根据传入的参数来初始化当前实例,那么就可以令其返回nil/0:

- (id)initWithValue:(id)value {
	if ((self = [super init])) {
		if (/* Value means instance can't bo created */) {
			self = nil;
		} elsef {
			//Initialize instance
		}
	}
	return self;
}

这样一来,如果if语句发现无法用传入的参数值来初始化当前实例,就会把self设置成nil,这样的话,整个方法的返回值也就是nil了,调用者发现初始化方法并没有把实例创建好,于是便可确定其中发生了错误。
NSError的用法更加灵活,我们可以经由此对象,把导致错误的原因回报给调用者。NSError对象里封装了三条信息:

  1. Error domain(错误范围,其类型为字符串)
  2. Error code (错误码,其类型为整数)
  3. User info(用户信息,其类型为字典)
    在设计API时,NSError的第一种常见用法时通过委托协议来传递此错误。有错误发生时,当前对象会把错误信息经由协议中的某个方法传给其委托对象。

NSError的另外一种常见用法是:经由方法的“输出参数”返回给调用者。

- (BOOL)doSomething:(NSError **)error

也可以把它当成一个直接指向NSError对象的指针,该类通过指针来传输数据。
下列代码吧NSError对象传递到“输出参数”中:

- (BOOL)doSomething:(NSError **)error {
	//Do something that may cause an error
	
	if (/ * there was an error * /) {
		if (error) {
			//Pass the 'error' through the out - paremeter
			*error = [NSErrorerrorWithDomain:domain code:code userInfo:userInfo];
		}	
		return NO;//Indicate failure
	} else {
		return YES;//Indicate success
	}
}

这段代码以*error语法为error参数“解引用”,也就是说,error所值的那个指针现在要指向一个新的NSError对象了,在解引用之前,必须先保证error参数不是nil,因为空指针解引用会导致“段错误”并使应用程序崩溃。调用者在不关心具体错误时,会给error参数传入nil,所以必须判断这种情况。
NSError对象里的“错误范围”、“错误码”、“用户信息”等部分应该按照具体的错误情况填入适当内容。这样调用者就可以根据错误类型分别处理各种错误了。例如,可以把错误码定义成下面:

//EOCErrors.h
extern NSString *const EOCErrorDomain;

typedef NS_ENUM(NSUInterger, EOCError) {
	EOCErrorUnknown                = -1
	EOCErrorInternalInconsistency  = 100
	EOCErrorGeneralFault           = 105;
	EOCErrorBadInput               = 500;
}
//EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain";

最好能为自己的程序库中所发生的错误指定一个“错误范围”字符串,使用此字符串创建NSError对象,并将其返回给库的使用者,这样的话,它们就能确信:该错误肯定是由你的程序库所回报的,用枚举类型来表示错误码也是明智之举,因为这些枚举不仅解释了错误码的含义,而且还给它们起了个有意义的名字。此外,也可以在定义这些枚举的头文件里对每个错误类型详加说明。
要点:

  1. 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
  2. 在错误不那么严重的情况下,可以指派“委托方法”来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。

九、理解NSCopying协议

NSCopying协议里有两个方法:

- (id) copyWithZone: (NSZone *)zone
- (id) mutableCopyWithZone: (NSZone *) zone 

分别用来实现浅拷贝和深拷贝,其详细知识点见下方博客:
属性关键字和深浅拷贝
要点:

  1. 若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。
  2. 如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议。
  3. 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
  4. 如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值