学习记录
- 15.用前缀避免命名空间冲突
- 16.提供“全能初始化方法”
- 17.实现description方法
- debugDescription:
- 18.尽量使用不可变对象
- 19.使用清晰而协调的命名方式
- 类与协议的命名
- 20.为私有方法名加前缀
- 21.理解OC错误模型
- 22.理解NSCopying协议
- 深拷贝和浅拷贝
15.用前缀避免命名空间冲突
由于OC没有像其他语言那样内置的命名空间,所以我们需要避免潜在的命名冲突。如果发生命名冲突,那么应用程序的链接过程就会出错。因为其中出现了重复符号:
错误在于,应用程序中的两份代码都各自实现了名为EOCTheClass的类。这导致EOCTheClass所对应的类符号和“元类”符号各定义了两次。
甚至可能出现更糟糕的情况:在运行期载入了含有重名类的程序库,此时,“动态加载器”就遭遇了“重名符号错误”,很可能导致整个程序崩溃。
避免此情况的唯一办法就是变相实现命名空间:为所有的名称都加上适当前缀。所选前缀可以是与公司、应用程序、或二者皆有关联之名
使用Cocoa创建应用程序时一定要注意,Apple宣称其保留使用所有“两字母前缀”的权利,所以你自己选用的前缀应该是三个字母的。
不仅是类名,应用程序中的所有名称都应加前缀。如果要为既有的类新增分类,那么一定要给“分类”及“分类”中的方法加上前缀,第25条解释了这么做的原因。
- 选择与你的公司、应用程序、或二者皆有关联之名称作为类名前缀,并在所有代码中使用这一前缀。
- 若自己开发的程序库中用到了第三方库,则应为其中的名称加上前缀。
16.提供“全能初始化方法”
所有的对象都得初始化。初始化时一般要提供一些额外信息,UITableViewCell初始化时就要知名样式及标识符,标识符能够区分不同类型的单元格。这种对象的创建成本较高,我们在创建的时候可以依照标识符来复用,以提升程序效率。我们把这种可以为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”
创建类实例的方式不止一种,那么这个类就会有多个初始化方法,不过仍然要在其中选一个作为全能初始化方法,令其他初始化方法最终都调用它。NSDate是一个例子,其初始化方法如下:
正如该类的文档描述的那样,在上面几个初始化方法中,“initWithTimeIntervalSinceReferenceDate:”是初始化方法,也就是说,其余的初始化方法都调用它。,于是,只有在全能初始化方法中,才会存储内部数据。这样的话,当底层存储机制改变时,只需修改此方法的代码就好,不需要改其他方法的代码。
比如说,要写一个表示矩形的类,其接口可以这样写:
#import<Foundation/Foundation.h>
@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
@end
根据18条中的建议,我们把属性声明为只读。不过这样一来,外界就无法设置EOCRectangle的属性了。开发者可能会提供初始化方法以设置这两个属性:
- (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等价的值)。如果把alloc方法分配好的EOCRectangle交由此方法来初始化,那么矩形的宽度与高度就是0,因为全部实例变量都设为0了。这也可能正是你想要的效果,不过此时我们一般希望能自己设定默认的宽度与高度值,或是抛出异常,指明本类实例必须用“全能初始化方法”来初始化。也就是说,在EOCRectangle这个例子中,应该像下面这样,参照其中一种版本来覆写init方法:
- (id)init{
return [self initWithWidth:5.0f andHeight:10.0f];
}
- (id)init{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:andHeight: instead." userInfo:nil]
}
每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上,实现“initWithCoder:”方法时也要这样,应该先调用超类的相关方法,然后再执行与本类有关的任务。这样编写出来的EOCSquare类就完全遵守NSCoding协议了。如果编写“initWithCoder:”方法时没有调用超类的同名方法,而是调用了自制的初始化方法,或是超类的其他初始化方法,那么EOCRectangle类的“initWithCoder:”方法就没机会执行,于是,也就无法将_width和_height两个实例变量解码了。
- 在类中提供一个全能初始化方法,并与文档里指明。其他初始化方法均应调用此方法。
- 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
- 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
17.实现description方法
调试程序时,经常需要打印并查看对象信息。一种办法是编写代码把对象的全部属性都输出到日志中。不过最常用的办法还是如下:
NSLog(@"object = %@", object);
在构建需要打印到日志的字符串时,object对象会收到description消息,该方法所返回的描述信息将取代“格式字符串”里的%@。比方说,object是个数组,若用下列代码打印其信息:
NSArray *object = @[@"A string", @(123)];
NSLog(@"object = %@", object);
输出:
object = {
"A string"
123
}
如果是一个自定义的类,则输出:
object = <EOCPerson: 0x7fd9a1600600>
如果我们想让其输出其他信息,就需要重写description方法:
- (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;
}>
debugDescription:
这个也是一种描述方法,和description差不多,就是描述的位置不一样,description是在函数调用类的时候触发方法才输出的,而debugDescription是在控制台中使用命令打印该对象时才调用的。当然加断点查看时也可以看到debugDescription的描述。
如果你在description不想将一些内容输出的话,你就可以将那些数据写在debugDescription中,让程序员自己调试时可以方便的看到这些数据,而description方法就输出你想要让用户看到的信息就行了。
- 实现description方法返回一个有意义的字符串,用以描述该实例。
- 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法。
18.尽量使用不可变对象
不可变对象,我们第一时间想到的肯定是不可变数组那种不可变对象,但是这里的不可变不是这样的,它指的是这个类里边的属性是不能直接被修改的,要实现这种功能,我们就需要用到我们的readonly(只读)修饰符。默认情况下,属性是readwrite(即可读又可写)的,这样修饰出来的类都是“可变的”。所以需要设置为readonly,就像这样:
@property (nonatomic, copy, readonly) NSString *identifier;
@property (nonatomic, copy, readonly) NSString *title;
@property (nonatomic, assign, readonly) float latitude;
现在,这个属性就只能用在实现代码内部设置这些属性了,但其实,在对象外部还可以通过“键值编码”技术来设置这些属性,就像“setValue:forKey:”方法。“点语法”也可以,因为点语法就是调用set方法的。这样做虽说可以改动,但是却违背了本心,还会导致数据不同而出现问题,所以不建议更改。
[pointOfInterest setValue:@"abc" forKey:@"identifier"];
这样子可以改动属性值,因为KVC会在类里查找“setIdentifier:”方法,并借此修改此属性。即使没有于公共接口中公布此方法,它也依然包含在类中。不过,这样做等于违规地绕过了本类所提供的API,要是开发者使用这种“杂技代码”的话,那么得自己开应对可能出现的问题。
还有一种可以修改数据的方法就是直接用类型信息查询功能查出属性所对应的实例变量在内存布局中的偏移量,以此来人为设置这个实例变量的值。这样做比绕过本类的公共API还要不合规范。所以不应该因为这个原因而忽视所提的建议,大家还是要尽量编写不可变的对象。
- 尽量创建不可变的对象
- 若某属性进可于对象内部修改,则在“分类”中将其由属性扩展为readwrite属性
- 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。
19.使用清晰而协调的命名方式
类,方法,变量的命名是Objective-C编程的重要环节。名称中一般都带有“in”,“for”,“with”等介词
方法与变量名应使用“驼峰命名法”,以小字母开头,其后每个单词首字母大写。类名也使用驼峰命名法,只是首字母大写,前面通常有两三个前缀字母。
类与协议的命名
应该为类与协议的名称加上前缀,以避免命名空间冲突,而且应该像给方法起名时那样把词句组织好,使其从左至右读起来较为通顺。基本命名规则就是:命名方式应该一致,如果要从其他的类中继承子类,那么就要遵守其原本的命名惯例。 例如:UIView它的子类就应该是***View,表明其来历。
总结规则:
- 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还 有修饰语,例如localized String。属性的存取方法 不遵循这种命名方式,因为一般认 为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝, 我们也认为那相当 于原有的对象。这些存取方法应该按照其所对应的属性来命名。
- 应该把表示参数类型的名词放在参数前面。
- 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数, 则应该在动词后面加 上一个或多个名词。
- 不要使用str 这种简称,应该用string 这样的全称。
- Boolean 属性应加is 前缀。如果某方法返回非属性的Boolean 值,那么应该根据其功 能,选用has 或is 当前缀。
- 将get 这个前缀留给那些借由“输出参数〞来保存返回值的方法,比如说,把返回值填充到〝C语言式数组” ( C - style array ) 里的那种方法就可以使用这个词做前缀 。
- 起名时应遵从标准的OC命名规范,这样创建出来的接口更容易为开发者所理解。
- 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
方法名里不要使用缩略后的类型名称。 - 给方法名起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。
20.为私有方法名加前缀
通常我们在写方法时,并没有对其进行私有共有分类,导致调试时可能很麻烦,现在为私有方法加上前缀,这样便于修改方法或方法签名。唯一注意的是:一定不要只使用_作为前缀,因为苹果公司使用的就是_作为私有方法的前缀的,你自己定义的私有方法名有可能就会和人家自带的冲突。
- 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
- 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。
21.理解OC错误模型
对于ARC,在默认情况下不是“异常安全的”。具体来说意味着如果抛出异常,本应该在作用区域未必释放的对象现在却不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现。即使不用ARC,也很难写出在抛出异常时不会导致内存泄漏的代码。如果有段代码先创建好了某个资源,使用完了之后在将其释放,可是在释放资源之前如果抛出异常了,该资源就不会被释放。
OC采用的办法是:只在极其罕见的情况下抛出异常,异常抛出之后,无需考虑恢复问题,而且程序此时应该退出。
异常值一个应用于极其严重的错误。出现不那么严重的错误时,OC语言所采用的变成范式为:令方法返回nil/0,或者使用NSError,表示其中有错误发生
- 只有发生了可使整个应用程序都崩溃的严重错误时,才应使用异常。
- 在错误不那么严重的情况下,可以指派“委托方法”来处理错误,也可以把错误信息放在NSError对象里面,经由“输出参数”返回给调用者。
22.理解NSCopying协议
使用对象时经常需要拷贝它,在OC中,此操作通过copy方法来完成。如果想令自己的类支持拷贝操作,那就要实现NSCopying协议,该协议只有一个方法:
- (id) copyWithZone : (NSZone*) zone;
为什么会出现NSZone呢?因为在以前开发程序时,会据此把内存分成不同的“区”(Zone),而对象会创建在某个区里面,现在已经不需要了,每个程序只有一个区:“默认区”(default zone)。所以说,尽管必须实现这个方法,但是你不必担心其中的Zone参数。
copy方法由NSObject实现,该方法只是以“默认区”为参数来调用上面的方法。我们总是想覆写copy方法,其实真正需要实现的却是“copyWithZone:”方法。
如果想使某个类支持拷贝,只需声明该类遵循NSCopying协议,并实现其中的那个方法即可。比方说有个表示个人信息的类,可以在其接口定义中声明此类遵从NSCopying协议:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString* firstName;
@property (nonatomic, copy, readonly) NSString* lastName;
- (id) initWithFirstName: (NSString*) firstName
andLastName: (NSString*) lastName;
@end
然后实现协议中规定的方法(copyWithZone:)。
- (id) copyWithZone:(NSZone *)zone {
EOCPerson* copy = [[[self class] allocWithZone:zone]
initWithFirstName:_firstName andLastName:_lastName];
return copy;
}
在本例实现的“copyWithZone:”中,我们直接把待拷贝的对象交给全能初始化方法,令其执行所有初始化工作。
深拷贝和浅拷贝
深拷贝:在拷贝对象自身时,将其底层数据也一并复制过去。
浅拷贝:只拷贝容器对象本身,不复制其中的数据。
- 若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。
- 如果自定义的对象分为可变版本和不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议。
- 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
- 如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。