接口与API设计
15、用前缀避免命名空间冲突
前言
开源社区以及开源组件随着iOS开发而流行起来,所以我们 经常会在开发自己的应用程序时使用他人的代码。所以我们要把代码写的清晰一些,以便其他开发者能够迅速而方便的将其集成到他们的项目里。
- OC没有其他语言那种内置的命名空间机制,所以我们在起名时要设法避免潜在的命名冲突。如果发生命名冲突,那么应用程序的链接过程就会出错。
- 比无法链接更糟糕的情况是,在运行期载入了含有重名类的程序库。此时,“动态加载器”就遭遇了“重名符号错误”,很可能令整个程序崩溃
解决方法:
- 为所用名称都加上适当前缀,可以是与公司、应用程序或二者皆有关联的。虽然这样也不能完全避免出现命名冲突但概率会小很多。
- 使用Cocoa创建应用程序时一定要注意,Apple宣称其保留使用所有“两字母前缀”的权利,所以你自己选用的应该是三个字母的。
- 不仅是类名应用程序中的所有名称都应加前缀。
要点:
- 选择与你的公司、应用程序或者二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀。
- 若自己所开发的程序中用到了第三方库,则应为其中的名称加上前面的前缀
16、提供“全能初始化方法”
- 所有对象都要初始化。
- 在初始化为对象提供必要信息以能完成工作的初始化方法叫做“全能初始化方法”。
- 每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上。
初始化过程:
1、自己的初始化方法中,优先调用父类的初始化方法
2、父类的初始化方法中在调用父类的初始化方法,依次往上调用
3、处于最上层的初始化完成后,回到第二层初始化方法中,完成第二层的初始化
4、第二层的初始化完成之后,再返回到第三层的初始化方法中,依次执行初始化。直到本类完成初始化方法。
学习继承之后,父类中声明了公共实例变量。作为父类,有自己的初始化方法,为这些公共实例变量赋值。子类定义了除了父类中的公共实例变量之外的实例变量。 在自身的初始化方法中,优先向super发送init方法,初始化公共变量。初始化成功后,再初始化自身特有的变量,从而完成所有的实例变量的初始化。
if (self = [super init]) { //给super发送init消息:即执行父类中的init方法
_width = width; //子类自己的初始化设置
_height = height;
}
return self; //初始化完成返回对象本身
}
它的子类的初始化方法应该怎么写呢?
#import "EOCRectangle.h"
NS_ASSUME_NONNULL_BEGIN
@interface EOCSquare : EOCRectangle
-(id)initWithDimension:(float)dimension;
@end
#import "EOCSquare.h"
@implementation EOCSquare
-(id)initWithDimension:(float)dimension {
return [super initWithWidth:dimension andHeight:dimension];
}
@end
上述方法就是EOCSquare
类的全能初始化方法。
它调用了父类的全能初始化方法。全能初始化方法的调用链一定要维系。
但是为什么不直接使用init
方法或者initWithWidth: andHeight:
来初始化EOCSquare
对象呢?
因为这样可能会创建出“宽度”“高度”不相等的正方形。
所以在类继承时需要注意:如果子类的全能初始化方法与超类方法名称不同,那就应该重写超类的全能初始化方法,即子类具有自己的实现,又有父类继承来的实现。
要点:
- 在类中提供一个全能初始化方法,并与文档里指明。其他初始化方法均应调用此方法。
- 若全能初始化方法与超类不同,则需要重写超类中对应方法。
- 如果超类的初始化方法不适用于子类,那么应该重写这个超类方法,并在其中抛出异常。
17、实现description方法
description方法:
使用NSLog和@%输出某个对象时,会调用类对象的description方法,并拿到返回值进行输出,把整个对象一次性打印出来。打印对象使用%@。
默认打印输出为<类名:内存地址>
那么应该怎么实现打印对象的所有属性呢?在类的实现中重写description方法。
- 实现description方法时没有固定规则,应根据当前对象来决定在description方法里打印何种信息。
- 想要在descrip中输出很多互不相同的信息可以借助NSDictionary类的description方法。
在自定义的description方法中把待打印的信息放到字典里然后将字典对象的descrip方法所输出的内容包含在字符串里并返回。
-(NSString*)description {
// return [NSString stringWithFormat:@"<FKApple[_color=%@,_weight=%g]>",self.color,self.weight];
return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self,@{@"color":self.color,@"weight":@(self.weight)}];
}
debugDescription:
此方法用意与description十分相似。二者区别在于debugDescription是开发者在调试器中以控制台命令打印对象时才调用的
操作:在创建实例所用代码后加断点,当程序运行到断电时,向调试器控制台里输入“po”命令就可以完成对象打印工作。
你可能不想把类名和指针地址这种额外内容放在普通的描述信息里,但却希望调试的时候能够很方便地看到它们就可以采取这样的方法。(Foundation框架的NSArray类就是这么做的)
要点:
- 实现description方法返回一个有意义的字符串,用以描述该实例
- 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription。
18、尽量使用不可变对象
前言
设计类的时候,应充分运用属性来封装数据。而在使用属性时,则可将其声明为“只读”(read-only)。默认情况下,属性是即可读又可写的,这样设计出来的类都是“可变的”。
应尽量把对外公布出来的属性设为只读,且只在非常有必要的时候才将属性对外公布。如果有人试着改变属性值,那么编译的时候就会报错,确保开发者在使用对象时能肯定底层数据不会改变。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKApple : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString* firstName;
@property (nonatomic, copy, readonly) NSString* lastName;
-(id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;
@end
有时想修改封装在对象内部的数据但是却不想这些数据被外人所改动,通常做法是将readonly属性重新声明为readwrite。但如果该属性是nonatomic就可能会产生“竞争条件”。在对象内部写入某属性时对象外的观察者也许正读取该属性。要避免这种问题就需要“派发队列”等手段,将(包括对象内部的)所有数据存取操作都设为同步操作。
将属性在对象内部重新的readwrite这一操作可以写在“class-continuation分类”中,在公共接口中声明的属性可于此重新声明但属性的其他特质必须保持不变。
@interface FKApple ()
@property (nonatomic, copy, readwrite) NSString* firstName;
@property (nonatomic, copy, readwrite) NSString* lastName;
@end
这个属性就只能用在实现代码内部设置这些属性了,但其实,在对象外部还可以通过“键值编码”技术来设置这些属性,就像“setValue:forKey:
”方法。“点语法”也可以,因为点语法就是调用set方法的。这样做虽说可以改动,但是却违背了本心,还会导致数据不同而出现问题,所以不建议更改。
要点
- 尽量创建不可变的对象
- 若某属性进可于对象内部修改,则在“分类”中将其由属性扩展为readwrite属性
- 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。
19、使用清晰而协调的命名方法
- 在Objective-C里方法和变量名使用“驼峰式大小写命名法”——以小写字母开头,其后每一个单词的首字母大写。 类名也用驼峰命名法,不过其首字母要大写,而且前面还有三个前缀首字母。
- 使用长名字可令代码更为易读,能够清楚的知道代码的用途。
- 方法命名时的几条注意事项:
- 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式,因为一般认为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其所对应的属性来命名。
- 应该把表示参数类型的名词放在参数前面。
- 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。
- 不要使用str这种简称,应该用string这样的全称。
- Boolean属性应加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功能,选用has或is当前缀。
Boolean类型的变量可以存储true或者flase
一般情况下Boolean类型的变量用来存储条件表达式的结果.如果条件表达式成立那么结果就是true如果条件表达式不成立结果过就是false
- 将get这个前缀留给那些借由“输出参数”来保存返回值的方法,比如说,把返回值填充到“C语言式数组" (C-style array)里的那种方法就可以使用这个词做前缀。
- 应该为类与协议的名称前加上前缀,以避免命名空间冲突且应该像给方法起名时那样把词句组织好,使其从左向右读起来通顺。
- 创建自定义的委托协议,则其名称中应该包含委托发起方的名称,后面再跟上
delegate
。
要点:
- 起名时应遵从标准的Objective-C命名规范,这样创建出来的接口更容易为开发者所理解。
- 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
- 方法名里不要使用缩略后的类型名称。
- 给方法起名时的第一 要务就是确保其风格与你自己的代码或所要集成的框架相符。
20、为私有方法名加前缀
为私有方法加前缀的原因:
- 编写类实现代码时经常要写一些只在内部使用的方法,加上前缀易于将公共方法和私有方法分开
- 便于修改方法名或方法签名。对于公共方法不应轻易改动。
- 具体使用何种前缀可根据个人喜好,最好包含下划线和字母,例如p_
- 与公共方法不同,私有方法不出现在接口定义中。有时要在“class-continuation分类”里声明私有方法,然而最近修订的编译器不要求在使用方法前必须先行声明了,所以说,私有方法一般只在实现的时候声明。
- 在Objective-C中,没办法真正把方法私有化,每个对象都可以响应任意消息,而且可以在运行期检视某个对象所能直接响应的消息。
- 为了避免重写苹果公司提供的私有化方法,所以不要用下划线做前缀。
要点:
- 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
- 不要单用一个下划线作私有方法的前缀,因为这种方法是预留给苹果公司用的。
21、理解Objective—C
错误模型
- “自动引用计数”在默认情况下不是“异常安全的”,具体来说,这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常之前,也照样要执行这部分代码,需要打开的编译器标志叫做
-fobjc-are-exceptions
。 Objective-C
语言所采用的方法是是:只在及其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且这时应用程序也应该退出了,所以也就不用写“异常安全代码”了。- 异常只用于处理严重错误,在出现其他不那么严重的错误时,
Objective-C
语言所用的编程范式为:令方法返回nil/0
,或是使用NSError
,以表明其中有错误发生。
- (id)initWithValue:(id)value {
if (self = [super init]) {
if (/*Value means instance can't be created*/) {
self = nil;
} else {
//Initialize instance
}
}
return self;
}
这种情况下,如果if语句发现无法用传入的参数值来初始化当前实例,那么就把self设置成nil,这样的话,整个方法的返回值也就是nil了。调用者发现初始化方法并没有把实例创建好,于是便可以知道其中发生了错误
NSError的用法更加灵活,因为经由此对象,我们可以把导致错误的原因回报给调用者
NSError
对象里封装了三条消息:
- 在设计API的时候,
NSError
的第一种常见的用法是通过委托协议来传递此错误,有错误发生时,当前对象会把错误信息经由协议中的某个方法传给其委托对象。 NSError
的另外一种常见用法是:经由方法的“输出参数”返回给调用者。- 为
error
参数“解引用(error
所指的那个指针现在要指向一个新的NSError
对象)”之前,必须先保证error
参数不是nil
,因为空指针解引用会导致段错误并使程序崩溃,所以一定要判断error
是不是nil
。
22、理解NSCopying协议
前言
使用对象时经常需要拷贝它。在Objective- C
中,此操作通过copy
方法完成。如果想令自己的类支持拷贝操作只需遵循NSCopying
协议,并实现其中的方法。
该协议只有一个方法
-(id)copyWithZone:(NSZone*)zone
为啥会出现NSZone呢?
因为以前开发程序时,会把内存分成不同的“区”(zone),而对象创建会创建在某个区里。现在不用了,每个程序都只有一个区:“默认区”(default zone)。所以尽管必须实现这个方法但是不必担心其中zone
的参数。
-(id)copyWithZone:(NSZone *)zone {
FKApple* copy = [[[self class] allocWithZone:zone]initWithFirstName:_firstName andLastName:_lastName];
return copy;
}
- mutableCopy这个“辅助方法”(helper)与copy相似,也是用默认的zone参数来调
mutableCopyWithZone:
。如果你的类分为可变版本与不可变版本,那么就应该实现NSMutableCopying。若采用此模式,在可变类中复写“copyWithZone:”方法时,要返回不可变的版本。无论当前实例是否可变,若需获取其可变版本都应该调用mutableCopy方法。同理,若需要不可变的拷贝,则应通过copy方法。
对于不可变的NSArray与可变的NSMutableArray来说,下列关系总是成立的:
[NSMutableArray copy] => NSArray
[NSArray mutableCopy] => NSMutableArray
2.另一种方案:提供三种方法:copy
、immutableCopy
、mutableCopy
。
copy
:返回的拷贝对象与当前对象类型一致。
immutableCopy
:返回不可变类型
mutableCopy
:返回可变类型
缺点:如果调用者不知道所用实例是否可变,这种方法就不太好了。某个方法可能会把NSMutableArray对象当作NSArray返回给你,而你在上面调用copy方法复制,此时你以为拷贝后的对象是不可变的数组,但实际上他是可变的。
在编写拷贝方法时,还要决定应该执行“深拷贝”还是“浅拷贝”
深拷贝:在拷贝对象自身时,将其底层数据也一并复制过去,也就是在堆里新开辟一块内存存放底层数据。
浅拷贝:只是拷贝容器对象本身,而不复制其中数据,也就是只拷贝存放在栈上指向堆内存的指针。
要点:
- 若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。
- 如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与NSMutableCopying 协议。
- 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
- 如果你所写的对象需要深拷贝, 那么可考虑新增一个专门执行深拷贝 的方法。