[iOS] Effective Objective-C —— 接
15. 用前缀避免命名空间冲突
两个不同的库如果有相同的类名文件,或不同类声明了相同名字的全局变量都会报错duplicate,为了避免这个问题就需要通过命名中加入公司、项目等前缀的方式尽量避免重复。
使用 Cocoa 创建应用程序时一定要注意,Apple 宣称其保留使用所有 “两字母前缀”(two-letter prefix)的权利,所以你自己选用的前缀应该是三个字母的。
举个例子,加入开发者不遵循这条守则,使用 TW 这两个字母作前缀,那么就会出问题。iOS 5.0 SDK 发布时,包含了 Twitter 框架,此框架就使用 TW 作前缀,其中有个类叫做 TWRequest,它可以发送 HTTP 请求以调用 Twitter API。如果你所在的公司叫做 Tiny Widgets,那么很有可能把访问本公司 API 所用的那个类也命名为 TWRequest。
※ 不仅是类名,应用程序中的所有名称都应加前缀。如果要为既有类新增 “分类”(category),那么一定要给 “分类”及“分类”中的方法加上前缀。开发者可能会忽略另外一个容易引发命名冲突的地方,那就是类的实现文件中所用的全局函数及全局变量(即使没有在.h文件中他也是全局的)
如果我们是写库给其他人用的,并且我们有引入第三方库就要特别注意啦。
举个例子,如果我们的项目里面拷贝了所有ABCLibrary的代码,然后有个人引入了我们的项目,但是他们同时引入了另外一个拷贝了ABCLibrary的项目的库,那么他的代码里面将有两个一模一样的ABCLibrary,必然会导致无法编译。
所以如果引入别人的库,需要把所有他们的的类、变量、方法等都在加上我们自己的前缀,类似XXXABCXXXClass。
当然这个主要场景是当你需要把自己的代码也作为库给别人用的时候,并且拷贝了其他库的代码。
16. 提供全能初始化方法
我们把可为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(designated initializer)。
如果类有多个初始化方法,那么需要在其中选定一个作为“全能初始化方法”,令其他初始化方法都来调用它。
于是,只有在全能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需要修改此方法的代码就好,无需改动其他初始化方法。
注意要覆盖init方法哦,否则外边直接alloc init就没有被cover啦,要不就转调用全能初始化,要不就抛异常
@interface EOCRectangle : NSObject
@property (nonatomic,readonly) float width;
@property (nonatomic,readonly) float height;
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
@end
@implementation EOCRectangle
- (instancetype)init{
return [self initWithWidth:500 height:500];
}
// 全能初始化方法
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height
{
self = [super init];
if (self) {
_width = width;
_height = height;
}
return self;
}
@end
类继承时需要注意的一个重要问题:如果子类的全能初始化方法与超类方法的名称不同,那么总应覆写超类的全能初始化方法。
也就是说,如果超类中的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
@interface EOCSquare : EOCRectangle
- (instancetype)initWithDimension:(CGFloat)dimension;
@end
@implementation EOCSquare
//如果使用者继续使用父类的全能初始化方法呢,这样就有可能出现宽高不等的正方形。所以还应该阻止使用者直接调用父类的全能初始化方法
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must be use initWithDimension :instead" userInfo:nil];
}
- (instancetype)initWithDimension:(CGFloat)dimension
{
return [super initWithWidth:dimension height:dimension];
}
//如果使用者还是用init来创建,这样还是调用父类中的init方法,还是有可能出现长宽不等的情况,所以还应该复写一下init方法
-(instancetype)init{
return [self initWithDimension:500];
}
@end
如果有实现NSCoding协议,NSCoding协议的初始化方法没有调用本类的全能初始化方法,而是调用了超类的相关方法。然而,若超类也实现了NSCoding,则需改为调用超类的"initWithCoder:"初始化方法。
// Initialiser from NSCoding,如果父类没有实现initWithCoder
- (id)initWithCoder:(NSCoder*)decoder {
// Call through to super’s designated initialiser
if ((self = [super init])) {
_width = [decoder decodeFloatForKey:@"width"];
_height = [decoder decodeFloatForKey:@"height"];
}
}
// NSCoding designated initialiser,如果父类实现了initWithCoder
- (id)initWithCoder:(NSCoder*)decoder {
if ((self = [super initWithCoder:decoder])) {
// EOCSquare’s specific initialiser
}
}
那么为什么不在initWithCoder中解码以后再调用全能初始化方法呢,这点我觉得是因为如果这样就要给每个参数建一个临时变量,然后最后调用全能init方法,比较冗余,直接设定属性比较节省。
17. 实现description方法
当我们想打印对象的时候经常调用:
NSLog(@"object = %@", object);
但是如果这个对象是自定义的,打印出来的就是指针,没有太大参考价值。
如果我们覆写description方法,那么就可以控制打印出来的是什么:
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, %@>", [self class], self, @{@"title": _title, @"latitude": @(_latitude), @"longitude": @(_longitude)}];
}
输出:
<EOCLocation: 0x60000002caa0, {
latitude = 34;
longitude = "163.45";
title = Beijing;
}>
用 NSDictionary 来实现此功能可以令代码更易维护: 如果以后还要向类中新增属性,并且要在 description 方法中打印,那么只需修改字典内容即可。
还有一个是dubugDescription方法,是开发者在调试器中以控制台命令打印对象时才调用的。使用LLDB的"po"命令可以完成打印工作,可以也覆写一下~
有的时候可能你希望使用object转为string的时候显示一些特定内容,用于编程时方便的显示在界面上,但是不要显示指针之类的,但在调试的时候又希望可以看到地址,那么就可以在dubugDescription中写带指针的,在description中写想每次app显示这个object的时候看到什么样的string。
18. 尽量使用不可变对象
能readonly就别readwrite,能不mutable就别mutable。
@property (nonatomic,copy,readonly) NSString *name;
@property (nonatomic,assign,readonly) NSInteger price;
那为啥readonly还要给出copy之类的修饰呢,之前有提过一个是为了语义清晰,在别人可能内部赋值的时候也知道需要copy后再赋值;另外还有一层考虑是如果想把这个变量改成readwrite是很好改的。
如果你又希望在.m文件中可以用self.的方式修改变量值,则可以在.m文件中重新声明,将其改为readwrite,但其他修饰符不可以不一致哦:
#import "EOCPointOfInterest.h"
@interface EOCPointOfInterest ()
@property (nonatomic, assign, readwrite) float latitude;
@property (nonatomic, assign, readwrite) float longitude;
@end
@implementation EOCPointOfInterest
…
@end
只是这样如果是nonatomic的属性,可能会有外面读的时候里面在写的冲突问题,可以用串行队列解决。
外部如果用KVC是可以改这个属性的,但一般其实不太推荐KVC,毕竟你不确定别人在setter里面做了什么其他的事情,以及将来他会怎么改setter。
※ 将不可变的collection对外
例如,我们用某个类来表示个人信息,该类里还存放了一些引用,指向此人的诸位朋友。你可能想把这个人的全部朋友都放在一个"列表"(list)里,并将其做成属性。假如开发者可以添加或删除此人的朋友,那么这个属性就需要用可变的set来实现。在这种情况下,通常应该提供一个readonly属性供外界使用,该属性将返回不可变的set,而此set则是内部那个可变set的一份拷贝。比方说,下面这段代码就能够实现出这样一个类:
// EOCPerson.h
#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)removeFriend:(EOCPerson*)person;
@end
// EOCPerson.m
#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
这样做的好处是,外部不能直接拿到可变的friend list的set,然后随意修改,只有内部可以对set进行修改,不用担心外部随意改动它。
如果使用别人的类,不要去判断声明为不mutable的属性是不是mutable,然后做修改。假设在.h文件中声明的是NSArray,实际上赋值了一个NSMutableArray,其实这个类内部并没有意料到外部会拿着这个array进行修改,这样做可能会有奇怪的事情发生,所以最好不要这么做,要避免下面的方式:
EOCPerson *person = …;
NSSet *friends = person.friends;
if ([friends isKindOfClass:[NSMutableSet class]]) {
NSMutableSet *mutableFriends = (NSMutableSet*)friends;
/* mutate the set */
}
19. 使用清晰而协调的命名方式
※ 方法名
不要吝于使用长方法名。把方法名起得稍微长一点,可以保证其能准确的传达出方法所执行的任务。然而方法名也不能长得太过分了,应尽量言简意骇。
以 EOCRectangle 类为例。好的方法名应该像这样:
- (EOCRectangle *)unionRectangle:(EOCRectangle *)rectangle;
- (float)area;
而下面这种命名方式则不好:(第一个方法名不清晰,第二个太啰嗦)
- (EOCRectangle *)union:(EOCRectangle *)rectangle; // Unclear
- (float)calculateTheArea; // Too verbose
NSString好的方法名:
+ stringWithString
工厂方法,根据某字符串创建出与之内容相同的新字符串。与创建空字符串所用的那个工厂方法一样,方法名的第一个单词也指明了返回类型。
+ intValue
将字符串解析为整数。由于返回值是 int,所以方法名以这个词开头。通常情况下我们不会像这样来简写返回值的类型,比如 string 不简写为 str,然而由于 “Integer”(整数)一词的简称 “int” 本身就是返回值的类型名称,所以此处这么做是合理的。为了把方法名凑足两个词,所以加了后缀 “Value”。只有一个词的名字通常用来表示属性。由于 int 不是字符串对象的属性,所以要加 Value 以限定其含义。
- length
获取字符串长度(也就是其字符个数)。这个方法只有一个词,因为实际上 length 也是字符串的一个属性。这个属性可能不是由实例变量来实现的,然而即便如此,它也依然是字符串中的属性。此方法若是命名为 stringLength 就不好了。 string 一词多余,因为该方法的接收者肯定是个字符串。
- hasPrefix:
判断本字符串是否以另一个字符串开头。由于返回值是 Boolean 类型,所以为了读起来像个句子。
要是把方法名直接写成 “prefix:”,读起来就不这么顺了。反之若将其叫成 “isPrefixedWith:”,则听上去冗长而别扭。
- getCharacters:range:
获取字符串中给定范围内的字符。其他语言里的获取方法也许会以 get 开头,但 Objective-C 中一般不这么做,然而此处例外,该方法用 get 作其前缀。原因在于,调用此方法时,要在其首个参数中传入数组,而该方法所获取的字符正是要放到这个数组里面。
原则就是:
- 如果方法的返回值是新创建的(例如工厂构造方法),那么方法名的首个词应是返回值的类型,除非前面还有修饰语,例如 localizedString 。属性的存取方法不遵循这种命名方式,因为一般认为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其所对应的属性来命名。
- 应该把表示参数类型的名词放在参数前面。
如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。 - 不要使用 str 这种简称,应该用 string 这样的全称。
- Boolean 属性应加 is 前缀。如果某方法返回非属性的 Boolean 值,那么应该根据其功能,选用 has 或 is 当前缀。
- 将 get 这个前缀留给那些借由 “输出参数” 来保存返回值的方法,比如说,把返回值填充到 “C 语言式数组”(C-style array)里的那种方法就可以使用这个词做前缀。
※ 类名、协议名
应该为类与协议的名称加上前缀,以避免命名空间冲突(因为OC其实没有命名空间),而且应该像给方法起名时那样把词句组织好,使其从左至右读起来较为通顺。
UITableView(类)
这是一种特殊类型的视图,可以显示表格中的一系列条目。所以,它在超类(UIView)名称中的 View 一词前面加了 Table 这个修饰词,用以和其他类型的视图相区隔。在超类名称前加修饰语是一种常用的命名惯例。本类也可以叫做 UITable,不过这个名字无法完整传达出 “视图”这个概念。开发者必须查看接口声明方能确定这一点。比方说,想创建一个专门用来显示图像的表格视图,那么就可以将这个继承自 UITableView 的子类命名为 EOCImageTableView。不过这时要加上自己的前缀 EOC,而不是沿用超类的前缀 UI (UIKit 框架中的类以 UI 为前缀)。这么做的原因在于,你不应该把自己的类放到其他框架的命名空间里面。那些框架以后也许会新建同名的类。
命名方式应该协调一致。如果要从其他框架中继承子类,那么务必遵循其命名惯例,并替换为自己的前缀;若要创建自定义的委托协议,则其名称中应该包含委托发起方的名称,后面再跟上 Delegate 一词。
20. 为私有方法名加前缀
一个类经常要写一些只在内部使用的方法。建议应该为这种方法的名称加上某些前缀,好处:
- 有助于调试,因为这样容易区别公共方法和私有方法。
- 便于修改方法名或方法签名。修改公共方法,那么使用了这个类公共方法的代码都需要更新,而修改私有方法,只需要修改本类内部相关代码即可。用前缀把私有方法标出,容易看出哪些可以随意修改,哪些不应轻易改动。
- 继承第三方库类的时候,避免和他们的私有方法重名导致意外覆写。
前缀推荐为“p_”
#import "EOCObject.h"
@implementation EOCObject
- (void)publicMethod
{
}
- (void)p_privateMethod
{
}
@end
苹果内部代码是以_下划线作为私有方法前缀的,如果我们也用下划线,可能不经意的继承并覆写了超类的某个方法,导致调用时机不可预测。
如果写过C++或Java代码,你可能就会问了: 为什么要这样做呢?直接把方法声明成私有的不就好了吗?Objective-C语言没办法将方法标为私有。每个对象都可以响应任意消息,而且可在运行期检视某个对象所能直接响应的消息。根据给定的消息查出其对应的方法,这一工作要在运行期才能完成,所以Objective-C中没有那种约束方法调用的机制用以限定谁能调用此方法、能在哪个对象上调用此方法以及何时能调用此方法。
21. 理解Objective-C错误模型
ARC在默认情况下不是"异常安全的"(exception safe)。具体来说,这意味着: 如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。如果想生成"异常安全"的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exception。
但是Objective-C语言现在所采用的办法是: 只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说,不用再编写复杂的"异常安全"代码了。
异常只应该用于极其严重的错误,比如说,你编写了某个抽象基类,它的正确用法是先从中继承一个子类,然后使用这个子类。在这种情况下,如果有人直接使用了这个抽象基类,那么可以考虑抛出异常。与其他语言不同,Objective-C中没办法将某个类标识为"抽象类"。要想达成类似效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。
- (void)mustOverrideMethod {
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"%@ must be overridden", _cmd]
userInfo:nil];
}
在出现"不那么严重的错误"(nonfatal error, 非致命错误)时,Objective-C语言所用的编程范式为: 令方法返回nil/0,或是使用NSError,以表明其中有错误发生。
※ NSError
NSError的用法更加灵活,因为经由此对象,我们可以把导致错误的原因回报给调用者。NSError对象里封装了三条信息:
-
Error domain(错误范围,其类型为字符串)
错误发生的范围。也就是产生错误的根源,通常用一个特有的全局变量来定义。 -
Error code(错误码,其类型为整数)
独有的错误代码,用以指明在某个范围内具体发生了何种错误。 -
User info(用户信息,其类型为字典)
有关此错误的额外信息,其中或许包含一段"本地化的描述"(localized description),或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条"错误链"(chain of errors)。
两种将NSError返回给调用者的方式:
- 通过delegate
- 通过指针
- 先来看通过委托来传递错误信息的:
- (void)connection:(NSURLConnection *)connection didFail WithError:(NSError *)error
- 另一种使用指针:
- (BOOL)doSomethingError:(NSError**)error
传递给方法的参数是个指针,而该指针本身又指向另外一个指针,那个指针指向NSError对象。或者也可以把它当成一个直接指向NSError对象的指针。这样一来,此方法不仅能有普通的返回值,而且还能经由"输出参数"把NSError对象回传给调用者。
NSError *error = nil;
BOOL ret = [object doSomethingError:&error];
if (error) {
// There was an error
}
实际上,在使用ARC时,编译器会把方法签名中的NSError转换成NSError__autoreleasing,也就是说,指针所指的对象会在方法执行完毕后自动释放。这个对象必须自动释放,因为"doSomething:"方法不能保证其调用者可以把此方法中创建的NSError释放掉,所以必须加入autorelease。
- (BOOL)doSomethingError:(NSError**)error {
// Do something that may cause an error
if (/* there was an error */) {
if (error) {
*error = [NSError errorWithDomain:domain
code:code
userInfo:userInfo];
}
return NO;///< Indicate failure
} else {
return YES; ///< Indicate success
}
}
必须先保证error参数不是nil,因为空指针解引用会导致"段错误"(segmentation fault)并使应用程序崩溃。调用者在不关心具体错误时,会给error参数传入nil,所以必须判断这种情况。
定义一个error可以这么写~ 注意命名要有意义,使用者才能看得懂。
// 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";
22. 理解NSCopying协议
为何会出现NSZone呢?因为以前开发程序时,会据此把内存分成不同的"区"(zone),而对象会创建在某个区里面。现在不用了,每个程序只有一个区:"默认区"(default zone)。所以说,尽管必须实现这个方法,但是你不必担心其中的zone参数。
实现NSCopying例如:
- (id)copyWithZone:(NSZone*)zone {
Person *copy = [[[self class] allocWithZone:zone]
initWithFirstName:_firstName
andLastName:_lastName];
return copy;
}
调用了全能初始化方法。
但是有的时候全能初始化方法没有把所有属性都包含在内,有些是内部用的,例如之前person例子里面的friend list,这种时候也应该吧他的list copy过来。
- (id)copyWithZone:(NSZone*)zone {
EOCPerson *copy = [[[self class] allocWithZone:zone]
initWithFirstName:_firstName
andLastName:_lastName];
copy->_friends = [_friends mutableCopy];
return copy;
}
注意如果friends是不可变的,则无须复制,如果复制了,那么内存中将会有两个一模一样的set,反而造成浪费了;但如果是可变的,为了避免莫名被更改,必须要复制哦。
通常情况下,应该像本例这样,采用全能初始化方法来初始化待拷贝的对象。不过有些时候不能这么做,因为全能初始化方法会产生一些"副作用"(side effect),这些附加操作对目前要拷贝的对象无益。比如,初始化方法可能要设置一个复杂的内部数据结构,可是在拷贝后的对象中,这个数据结构立刻就要用其他数据来覆写,所以没必要再设置一遍。
NSSet有提供深拷贝的init方法:
- (id)initWithSet: (NSArray *)array copyItems:(BOOL)copyItems