EffectiveOC的学习-4

这里是EffectiveOC的 15 - 22条

2016-07-03 | 阅读

15. 用前缀避免命名空间冲突

Objective-C中没有像其他语言那样拥有命名空间,所以就很容易发生命名冲突,如 在一个项目的代码中有两个同名的类的实现 ,同名的类在程序编译时产生了重复符号,而导致链接出错,编译失败.

解决命名冲突一般通过前缀的方式,来变相实现命名空间,如Cocoa框架的NS UI CF CG等等. 由于Apple宣称其保留使用所有两个字母的前缀的权利,所以我们普通开发者应该使用三个字母的前缀来进行命名.

需要注意的是,命名冲突不止是类的命名 , 如在已有类上添加分类Category,则Category的命名也应该要拥有前缀, 如果分类中实现了一些方法,这些方法的命名也应该附上前缀. 还有就是 C函数的命名,C的函数,如果没有使用static声明,就表示这是一个全局的函数,如果不用前缀来限制,极容易产生冲突.

16. 提供 “全能初始化方法”

一个类的初始化方法会有很多个, 不同情况下传递不同的参数,都有相应的初始化函数,而建议提供一个designated initializer的初始化方法,让其他所有的初始化方法都调用这个指定的初始化方法. 在这个特定初始化方法中做一些统一的初始化工作。

NSDate,它有以下6种初始化方法 :

- (instancetype)init;
- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;
- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;

在6个函数中 initWithTimeIntervalSinceReferenceDate:designated initializer, 所有初始化函数都会先调用它 .

有些时候会有多个designated initializer, 如 UIView的初始化中,有两个这样的初始化函数 ,分别是 :

- (instancetype)initWithFrame:(CGRect)frame;
- (instancetype)initWithCoder:(NSCoder *)aDecoder;

前者是使用代码创建界面时, 初始化View一定会调用 initWithFrame:来初始化,而 initWithCoder:是在加载NIB XIB SB这些文件的界面时使用。

设计代码时,实现designated initializer是有意义的. 譬如在开发后期想要在初始化时修改一个新的属性或者调用某个方法,就可以统一在这个指定的初始化函数中处理,而不需要修改多个初始化函数.

17. 实现description 方法

在调试程序时,我们经常会打印出对象 ,如 NSLog(@"%@",object) , 这个时候打印的是[object description]的结果. 所以重写或实现这个方法是很有必要的,能够便于调试 . 一个常用的场景就是NSDictionary的打印, NSDictionary默认实现了description的打印,但是对中文不友好,打印出来的中文被转变为UTF-16编码了.我们通过一个NSDictionaryCategory中实现descriptionWithLocale:方法,来解决中文编码输出问题 :

- (NSString *)descriptionWithLocale:(nullable id)locale {
    NSMutableString *str = [[NSMutableString alloc] initWithString:@"Dictionary : { \n"];
    for (id key in self.allKeys) {
        [str appendFormat:@"[%@]:  %@",key,self[key]];
    }
    [str appendString:@" } "];
    return [str copy];
}

除了普通的description方法外,还有一个类似的方法debugDescription,这个方法是开发者在调试器中以控制台命令打印时才会调用.在lldb调试器中才会调用这个方法,可以在调试状态中打印与普通输出不同的内容.

18. 尽量使用不可变对象

对于基本不可变的属性,尽量使用readonly来声明其不可变. 设置属性只读,有很多好处, 最大一个好处在于面向对象中的封装的意义,将不应该暴露的部分封装起来,来让对象自身更好地控制自己. 简单来说,一个对象,如果有A,B,C三个属性,这三个属性互相有一定相互作用的关系, 如 C = A + B 这样的关系,而且在这个三个属性没有对外部暴露修改接口的情况下,显然是封装起来,设置属性只读,更加安全. 而如果类内部需要修改这些属性,可以考虑在class-continuation重新声明这些属性,或者直接操作这些属性的值(直接操作_A,_B,_C).

当然,封装属性,并不能阻止其他人修改这些属性,其他人可以通过其他的hack方法来修改这些属性, 如通过KVC, 或者直接寻找实例变量在内存中的偏移量来暴力修改属性的值.

对于类中使用一个集合来存放数据时, 如使用一个NSMutableDictionary或者一个NSMutableArray时,内部操作肯定是使用可变对象,但是返回给外部集合时,应该返回一个不变属性的集合,这样能防止其他人不经对象的相关方法而直接修改对象的集合属性. 这些类中,获取集合的方法,一般返回一个 可变的对象的 普通copy,如 [mutableList copy] . 而有些时候,你觉得每次复制一份完整的集合,是一件耗时的无用的操作时,你可以直接将 这个集合返回回去,而函数的返回值类型是一个不变的集合类型,即 - (NSArray)friendsList; . 调用者看到这种方法声明时,不应该自作聪明的去猜去试其内部实现 ,即使这个版本的这个接口,返回值真的是一个NSMutableArray的对象,也不应该去使用,因为在不可定的未来,返回值可能就会修改为 不可变的NSArray类型。 所以,对于调用其他人编写的接口时,要注意别人的声明, 只依赖别人的接口,而不要去依赖内部实现。

19. 使用清晰而协调的命名方式

命名是世界上的一大难题,OC的语法中的一个最大特点,就是方法可以分段,所以分段的用方法名来说明各个参数的含义,如对于一个Rectangle类,有一个方法 :

- (instancetype)initWithHeight:(float)height andWidth:(float)width;

这个方法名是 initWithHeght:andWidth: ,要注意OC中的方法名是包括这个:号的,将参数的含义放在函数名中,使函数命名更加清晰,也更加臃肿. 命名时,要遵循驼峰时命名方式.

对于方法的命名,需要注意以下几点:

  • 如果方法的返回值是一个新创建的对象,命名以名词结尾.
  • 将参数描述放在参数出现的位置之前,以在命名中说明参数的含义.
  • 执行操作的函数,命名中要包含动词,如果有参数,参数应该放在动词后面,用名词表明.
  • 不要在命名中使用str这种缩写,而要使用全称.
  • 判断的函数,应该以 has或者is作为前缀.

20. 为私有方法名加前缀

对于公有方法,修改其名称或具体功能时,使用到这个类的其他方法也可能需要进行改动,以支持这些改动.而私有方法是类内部自己的实现,修改私有方法后,只需要关注类本身相关代码的修改,不会影响到外部. 所以要有一种途径来判断哪些方法是私有方法,哪些方法是公有方法,即哪些方法可以随意改动,而哪些方法不应轻易更改.

建议使用p_来表示私有函数的前缀, 而Apple一般使用_来作为私有函数的前缀. 我们使用前缀来声明私有函数,只是在开发的一些提示的效果, OC中并没有私有函数的概念,所有的函数都在runtime中可以随意调用.

21.理解Objective-C错误模型

Apple对于异常与错误,给出的规范是 不需要复杂的异常安全的代码,异常只在很罕见的情况下抛出,这些异常是不可逆无法解决的异常,不要去考虑如何解决这些异常并保证程序正常运行,而是直接让程序退出.

这是苹果的官方做法,而一般开发者不会选择这种做法,异常导致应用崩溃,对开发者来说是不想看见的. 有些程序的做法是, 抓到异常后,程序已经无法正常运行下去,但是不让程序崩溃,而是让程序卡在那里, 这会让一般的用户以为是系统或者APP卡住了,然后他们会自行去手动操作关闭程序, 避免了程序自己的崩溃. 官方做法和常用做法,着重点不同,官方更在意用户体验,而后者在意的是APP的评价, 宁愿卡住,也不能让程序崩溃。

在OC中,对于普通的异常,即不会导致系统无法恢复的异常,一般使用NSError来进行封装,NSError中存放了三条信息:

  • Error domain : 字符串类型,表示错误的范围,记录错误发生的模块,类和具体方法等信息,用于定位.
  • Error code : 整型的错误码.
  • User info : 用户信息,字典类型,一般放一些描述具体错误内容的数据.

使用NSError,一般是以下形式:

- (void)doSomething:(NSError **)error;

NSError以指针的指针的形式作为函数的参数. 使用时, 使用者声明一个空的NSError的指针,作为参数调用方法,如果出错,在函数运行结束时,这个NSError的指针就正确地指向一个错误信息的对象了.

22. 理解NSCopying协议

NSCopying协议只有一个方法:

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

NSZone是一些历史遗留的内容,之前开发时,会将内存分成不同的区,而在现在,我们不需要关心这个zone参数.当我们要拷贝对象时,会调用NSObject中的copy方法,而该方法会调用NSCoping协议的这个方法来创建一个复制对象.

对应copy方法,还有一个mutableCopy的方法,也有一个对应的NSMutableCopying的协议中定义了mutableCopyWithZone:的方法.两个方法,copy是返回不可变版本的复制,而mutableCopy返回的是可变版本的复制 .

对于一个对于 NSMutableArray的对象,调用 copy后,返回的是一个 NSArray的对象,而调用mutableCopy返回的是一个NSMutableArray的对象. 而对于NSArray的对象,调用copy返回一个普通的NSArray对象,而调用mutableCopy返回一个NSMutableArray的对象.

深浅拷贝: 深拷贝是指将所有底层数据全部复制一份,而浅拷贝只是复制指针, 如对于一个 NSArray *listA = @[A,B] , ListA中含有两个对象,A和B, 浅复制指 NSArray *listB = @[A,B], listB中对象依旧指向A和B,即底层数据没有复制,而深复制中 NSArray *listC = @[A2,B2] , 复制list时,同时复制了A,B两个对象,导致listC并不与listA持有同一个对象. 深复制时,复制整个底层的数据结构.

绝大多数情况下,开发时只需要浅复制就足够了,而默认的基础数据类型实现的copymutableCopy都是浅复制, 需要注意这一点,复制的集合中持有的是原对象的指针.

还需要注意的是,基础类型(这里说的是 NSNumber,NSString,NSArray,NSDictionary等这些基础的OC类型)的copy方法, 对于不可变的类型操作时,如复制一个NSArray或者复制一个NSString时,事实上没有进行复制,只是复制指针, 即对于

NSArray *listA = @[A,B];
NSArray *listB = [listA copy];

listB复制了listA,但事实上只是复制了指针而已,NSArray *listB = [listA copy];实际上执行的操作是 NSArray *listB = listA; 而已, 并没有创建一个新的数组.

而可变的类型进行copy时,事实上进行了复制, 即对于一个NSMutableArray的对象进行复制时,使用浅复制,复制出一个NSArray的对象.