iOS-Effective Objective-C 2.0 读书笔记(三)

第三章的内容主要是说接口和API设计相关的注意事项。比如说我们自己写的代码需要设计以便于代码复用时,应该注意的一些问题包括哪些。

简单总结

  有些注意事项实际上很简单,而且很常见,我觉得并不需要太长篇幅来解释说明大家也能理解,所以这里主要以归纳的形式列出。

1.1用前缀命名方式避免命名空间冲突

  包括变量命名(尤其是全局变量,C函数等),类名,方法名,类别名等。作者推荐的方式是使用三个字母的缩写来表示前缀。对于引入的第三方库,一个最稳妥的方式则所有引入的库文件也都加上自己的前缀。

1.2 提供“指定初始化方法”(designated initializer)

  如果一个类有多个初始化方法,一般都会指定需要参数最多的那个方法为指定初始化方法,然后别的初始化方法调用该方法进行初始化。
  这里有一个需要注意的问题就是:如果子类的指定初始化方法与超类的指定初始化方法的名称不同,那么总应覆写超类的指定初始化方法,然后在其中调用子类指定初始化方法或者抛出异常。

1.3 实现description方法

  这个主要是为了方便调试时候打印的信息可读性更好,方便调试。因为默认的方法实现是有NSObject根类实现的,其只是简单输出了类名和实例的地址。对于我们自定义的子类,重写该方法,然后输出一些有用的信息能更好的辅助调试工作的进行。
  一个小技巧:如果需要输出一些属性值,可以把这些属性和值放到一个字典NSDictionary中,然后输出则是键值对的形式了。

1.4 尽量使用不可变对象

  这个建议的出发点可以用这么一个例子解释。如果把可变对象放入一个collection之后又修改其内容,那么很容易就会破坏set的内部数据结构。
  在具体编程时间中,则应该尽量把对外公布出来的属性设为只读,而且只在有必要的时候才将属性对外公布。(给出的例子是在.h文件中将属性设置为readonly,然后在实现文件的扩展或分类文件中,重新声明该变量并设置为readwrite)。但是对于KVC机制“暴力”修改变量值的行为还是不太好处理的鸟。
  如果有时候属性里有collection类型的对象时,通常会设置为reaonly,提供一个访问接口给外部,然后该属性再提供一个接口返回不可变的set,而且这个set只是内部那个可变set的一个拷贝。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//LYSPerson.h
@interface LYSPerson : NSObject
@property (noatomic, copy, readonly) NSString *firstName;
@property (noatomic, copy, readonly) NSString *lastName;
@property (noatomic, strong, readonly) NSSet *friends;
- (instancetype)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;
- (void)addFriend:(LYSPerson *)person;
- (void)removeFriend:(LYSPerson *)person;
@end
//LYSPerson.m
@interface LYSPerson ()
@property (noatomic, copy, readwrite) NSString *firstName;
@property (noatomic, copy, readwrite) NSString *lastName;
@end
@implementation LYSPerson
{
NSMutableSet *_internalFriends;
}
- (NSSet *)friends{
//返回一个不可变的拷贝对象
return [_internalFriends copy];
}
- (void)addFriend:(LYSPerson *)person{
[_internalFriends addObject:person];
}
- (void)removeFriend:(LYSPerson *)person{
[_internalFriends removeObject:person];
}
- (instancetype)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName
{
if(self = [super init]){
_firstName = firstName;
_lastName = lastName;
_internalFriends = [NSMutableSet new];
}
}
@end

  但是上面的做法还是有它的缺陷的,比如说并不能保证通过friends返回的数据一定和当前可变集合中的数据是一致的,并且如果集合很大的哈,拷贝也需要一定的花销。
  所以无论什么方案都会有其优点和有其缺点,所以需要根据实际情况具体分析。

1.5 OC错误模型

  这里所指的错误模型主要是指当程序运行过程中如果发生错误,我们应该怎么处理,比如说在其他类似java这样的语言中则会有很完善的抛出异常的机制。
  然而在OC里抛出异常不是“安全”的。就是说如果抛出异常,那么本应该在作用域末尾释放的对象现在却不会自动释放了。所以这是一个非常需要注意的事情。
  所以OC里所采用的方法主要是:只有在极其罕见的情况下才抛出异常,异常抛出之后,无需考虑恢复的问题,而且应用程序此时也应该退出。如果出现“不那么严重的错误”时,OC主要的方式是令方法返回nil/0,或是使用NSError,以表明其中有错误发生。
  在API设计中,NSError第一常见的用法是通过委托协议来传递错误。如果有错误发生时。当前对象会把错误信息经由协议中的某个方法传给其委托对象。比如说我们在网络请求时候涉及的类NSURLConnection在其委托协议中NSURLConnectionDelegate中就有如下定义:

1
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error

  当NSURLConnection发生错误时,就会调用该方法处理相关错误。这样做比抛出异常要好,因为调用者至少可以决定如何处理可能出现的错误。

  另一种常见的用法则是经由方法的参数返回给调用者。比如这样:

1
2
3
4
5
6
7
8
- (BOOL) doSomething:(NSError **)error
//使用
NSError *error = nil;
BOOL ret = [object doSomthing:&error];
if(error){
// 处理错误
}

  这种方式的好处就是不影响函数的返回值,同时如果需要关注函数出现的错误,只需要根据传入的参数来做相对的处理。

PS:这里回顾了一下,在函数里传一个对象的指针与传指向对象的指针的指针的区别。实际上第二种方式更灵活点,可以说其包括了前一种方式的功能,同时更重要的是其可以改变指向具体的错误对象的指针。或许我们可以这样理解,第一种方式可以改变我们传入的特定对象的内容,第二种方式不止能改变传入的对象的内容,甚至还可以重新指定该指针所指向的对象地址。