《禅与Objective-C编程艺术》读书笔记(一)
一、条件语句
1.书写规范
条件语句体应该总是被大括号包围来避免错误,即使可以不用(比如,只有一行内容)。这些错误包括多加了第二行,并且误以为它是 if 语句体里面的。此外,更危险的可能是,如果把 if 语句体里的一行注释掉了,之后的一行代码会不知不觉成为 if 语句里的代码。因此,条件语句应该使用被大括号包围的方式去写。
推荐:
if (!error) { return success; }
2.尤达表达式
不要使用尤达表达式。尤达表达式是指,拿一个常量去和变量比较而不是拿变量去和常量比较。它就像是在表达 “蓝色是不是天空的颜色” 或者 “高个是不是这个男人的属性” 而不是 “天空是不是蓝的” 或者 “这个男人是不是高个子的。
推荐:
if ([myvalue isequal:@42]) { ...
不推荐:
if ([@42 isequal:myvalue]) { ...
3.nil 和 bool 检查
检查途径是使用感叹号来判断。因为 nil 是 解释到 no 所以没必要在条件语句里面把它和其他值比较。同时,不要直接把它和 yes 比较,因为 yes 的定义是 1 而 bool 是 8 位的,实际上是 char 类型。
推荐:
if (someobject) { ... if (![someobject boolvalue]) { ... if (!someobject) { ...
不推荐:
if (someobject == yes) { ... // wrong if (myrawvalue == yes) { ... // never do this. if ([someobject boolvalue] == no) { ...
4.黄金大道
当编写条件语句的时候,左边的代码间距应该是一个“黄金”或者“快乐”的大道。 这是说,不要嵌套if语句。多个 return 语句是ok的。这样可以避免 cyclomatic 复杂性,并且让代码更加容易。因为你的方法的重要的部分没有嵌套在分支上,你可以很清楚地找到相关的代码。
推荐:
- (void)somemethod { if (![someother boolvalue]) { return; } //do something important }
不推荐:
- (void)somemethod { if ([someother boolvalue]) { //do something important } }
5.复杂的表达式
当你有一个复杂的 if 子句的时候,你应该把它们提取出来赋给一个 bool 变量,这样可以让逻辑更清楚,而且让每个子句的意义体现出来
bool namecontainsswift = [sessionname containsstring:@"swift"]; bool iscurrentyear = [sessiondatecompontents year] == 2014; bool isswiftsession = namecontainsswift && iscurrentyear; if (isswiftsession) { // do something very cool }
6.三元运算符
三元运算符 ? 应该只用在它能让代码更加清楚的地方。 一个条件语句的所有的变量应该是已经被求值了的。计算多个条件子句通常会让语句更加难以理解,就像if语句的情况一样,或者把它们重构到实例变量里面。
推荐:
result = a > b ? x : y;
不推荐:
result = a > b ? x = c > d ? c : d : y;
当三元运算符的第二个参数(if 分支)返回和条件语句中已经检查的对象一样的对象的时候,下面的表达方式更灵巧:
推荐:
result = object ? : [self createobject];
不推荐:
result = object ? object : [self createobject];
7.错误处理
当方法返回一个错误参数的引用的时候,检查返回值,而不是错误的变量。
推荐:
nserror *error = nil; if (![self trysomethingwitherror:&error]) { // handle error }
此外,一些苹果的 api 在成功的情况下会对 error 参数(如果它非 null)写入垃圾值(garbage values),所以如果检查 error 的值可能导致错误 (甚至崩溃)。
二、case语句
二、case语句
1.书写规范
除非编译器强制要求,括号在 case 语句里面是不必要的。但是当一个 case 包含了多行语句的时候,需要加上括号。有时候可以使用 fall-through 在不同的 case 里面执行一样的代码。一个 fall-through 是指移除 case 语句的 “break” 然后让下面的 case 继续执行。当在 switch 语句里面使用一个可枚举的变量的时候,default 是不必要的。此外,为了避免使用默认的 case,如果新的值加入到 enum,程序员会马上收到一个 warning 通知
enumeration value 'zocenumvalue3' not handled in switch.
2.enumerated types 枚举类型
当使用 enum 的时候,建议使用新的固定的基础类型定义,因它有更强大的的类型检查和代码补全。 sdk 现在有一个 宏来鼓励和促进使用固定类型定义 - ns_enum()
例子: *
typedef ns_enum(nsuinteger, zocmachinestate) { zocmachinestatenone, zocmachinestateidle, zocmachinestaterunning, zocmachinestatepaused };
三、命名
三、命名
1.通用的约定
尽可能遵守 apple 的命名约定,尤其是和 内存管理规则 (narc) 相关的地方。推荐使用长的、描述性的方法和变量名
推荐:
uibutton *settingsbutton;
不推荐:
uibutton *setbut;
2.constants 常量
常量应该使用驼峰命名法,并且为了清楚,应该用相关的类名作为前缀。
推荐:
static const nstimeinterval zocsigninviewcontrollerfadeoutanimationduration = 0.4;
不推荐:
static const nstimeinterval fadeouttime = 0.4;
常量应该尽量使用 in-line 的字符串字面值或者数字,这样便于经常用到的时候复用,并且可以快速修改而不用查找和替换。 常量应该用 static 声明,并且不要使用 #define,除非它就是明确作为一个宏来用的。
推荐:
static nsstring * const zoccachecontrollerdidclearcachenotification = @"zoccachecontrollerdidclearcachenotification"; static const cgfloat zocimagethumbnailheight = 50.0f;
不推荐:
#define companyname @"apple inc." #define magicnumber 42
常量应该在 interface 文件中这样被声明:
extern nsstring *const zoccachecontrollerdidclearcachenotification;
并且应该在实现文件中实现它的定义。你只需要为公开的常量添加命名空间前缀。即使私有常量在实现文件中可能以不同的模式使用,你也不需要坚持这个规则了。
3.方法
对于方法签名,在方法类型 (-/+ 符号)后应该要有一个空格。方法段之间也应该有一个空格(来符合 apple 的规范)。在参数名称之前总是应该有一个描述性的关键词。使用“and”命名的时候应当更加谨慎。它不应该用作阐明有多个参数,比如下面的initwithwidth:height: 例子:
推荐:
- (void)setexampletext:(nsstring *)text image:(uiimage *)image; - (void)sendaction:(sel)aselector to:(id)anobject forallcells:(bool)flag; - (id)viewwithtag:(nsinteger)tag; - (instancetype)initwithwidth:(cgfloat)width height:(cgfloat)height;
不推荐:
- (void)sett:(nsstring *)text i:(uiimage *)image; - (void)sendaction:(sel)aselector :(id)anobject :(bool)flag; - (id)taggedview:(nsinteger)tag; - (instancetype)initwithwidth:(cgfloat)width andheight:(cgfloat)height; - (instancetype)initwith:(int)width and:(int)height; // never do this.
4.字面值
nsstring, nsdictionary, nsarray, 和 nsnumber 字面值应该用在任何创建不可变的实例对象。特别小心 nil 不能放进 nsarray 和 nsdictionary 里,这会导致 crash。
例子:
nsarray *names = @[@"brian", @"matt", @"chris", @"alex", @"steve", @"paul"]; nsdictionary *productmanagers = @{@"iphone" : @"kate", @"ipad" : @"kamal", @"mobile web" : @"bill"}; nsnumber *shoulduseliterals = @yes; nsnumber *buildingzipcode = @10018;
不要这样做:
nsarray *names = [nsarray arraywithobjects:@"brian", @"matt", @"chris", @"alex", @"steve", @"paul", nil]; nsdictionary *productmanagers = [nsdictionary dictionarywithobjectsandkeys: @"kate", @"iphone", @"kamal", @"ipad", @"bill", @"mobile web", nil]; nsnumber *shoulduseliterals = [nsnumber numberwithbool:yes]; nsnumber *buildingzipcode = [nsnumber numberwithinteger:10018];
对于那些可变的副本,我们推荐使用明确的如 nsmutablearray, nsmutablestring 这些类。
下面的例子 应该被避免:
nsmutablearray *amutablearray = [@[] mutablecopy];
上面的书写方式存在效率以及可读性的问题。效率方面,一个不必要的不可变变量被创建,并且马上被废弃了;这并不会让你的 app 变得更慢(除非这个方法会被很频繁地调用),但是确实没必要为了少打几个字而这样做。对于可读性来说,存在两个问题:第一个是当浏览代码并且看见 @[] 的时候你的脑海里马上会联系到 nsarray 的实例,但是在这种情形下你需要停下来思考下。另一个方面,一些新手看到后可能会对可变和不可变对象的分歧感到不舒服。他/她可能对创造一个可变对象的副本不是很熟悉(当然这并不是说这个知识不重要)。当然,这并不是说存在绝对的错误,只是可用性(包括可读性)有一些问题。
四、类
四、类
1.类名
类名应加上 三 个大写字母作为前缀(两个字母的为 apple 的类保留)。虽然这个规范看起来难看,但是这样做是为了减少 objective-c 没有命名空间所带来的问题。
一些开发者在定义 model 对象时并不遵循这个规范(对于 core data 对象,我们更应该遵循这个规范)。我们建议在定义 core data 对象时严格遵循这个约定,因为你最后可能把你的 managed object model 和其他(第三方库)的 managed object model 合并。你可能注意到了,这本书里的类的前缀(其实不仅仅是类)是zoc。另一个类的命名规范:当你创建一个子类的时候,你应该把说明性的部分放在前缀和父类名的在中间。举个例子:如果你有一个 zocnetworkclient 类,子类的名字会是zoctwitternetworkclient (注意 “twitter” 在 “zoc” 和 “networkclient” 之间); 按照这个约定, 一个uiviewcontroller 的子类会是 zoctimelineviewcontroller.
2.initializer 和 dealloc 初始化
推荐的代码组织方式:将 dealloc 方法放在实现文件的最前面(直接在 @synthesize 以及 @dynamic 之后),init 应该放在 dealloc 之后。如果有多个初始化方法, designated initializer 应该放在第一个,secondary initializer 在之后紧随,这样逻辑性更好。 如今有了 arc,dealloc 方法几乎不需要实现,不过把 init 和 dealloc 放在一起可以从视觉上强调它们是一对的。通常,在 init 方法中做的事情需要在 dealloc 方法中撤销。
init 方法应该是这样的结构:
- (instancetype)init { self = [super init]; // call the designated initializer if (self) { // custom initialization } return self; }
为什么设置 self 为 [super init] 的返回值,以及中间发生了什么呢?这是一个十分有趣的话题。
让我们后退一步:我们曾经写了类似 [[nsobject alloc] init] 的表达式, alloc 和 init 区别慢慢褪去 。一个 objective-c 的特性叫 两步创建 。 这意味着申请分配内存和初始化是两个分离的操作。
(1)、alloc表示对象分配内存,这个过程涉及分配足够的可用内存来保存对象,写入isa指针,初始化 retain 的计数,并且初始化所有实例变量。
init 是表示初始化对象,这意味着把对象放到了一个可用的状态。这通常是指把对象的实例变量赋给了可用的值。
(2)、init 是表示初始化对象,这意味着把对象放到了一个可用的状态。这通常是指把对象的实例变量赋给了可用的值。
3.designated 和 secondary initializers
objective-c 有 designated 和 secondary 初始化方法的观念。 designated 初始化方法是提供所有的参数,secondary 初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用 designated 初始化方法的初始化方法。
@implementation zocevent - (instancetype)initwithtitle:(nsstring *)title date:(nsdate *)date location:(cllocation *)location { self = [super init]; if (self) { _title = title; _date = date; _location = location; } return self; } - (instancetype)initwithtitle:(nsstring *)title date:(nsdate *)date { return [self initwithtitle:title date:date location:nil]; } - (instancetype)initwithtitle:(nsstring *)title { return [self initwithtitle:title date:[nsdate date] location:nil]; } @end
initwithtitle:date:location: 就是 designated 初始化方法,另外的两个是 secondary 初始化方法。因为它们仅仅是调用类实现的 designated 初始化方法。一个类应该又且只有一个 designated 初始化方法,其他的初始化方法应该调用这个 designated 的初始化方法。
当定义一个新类的时候有三个不同的方式: 1. 不需要重载任何初始化函数 2. 重载 designated initializer 3. 定义一个新的 designated initializer。第一个方案是最简单的:你不需要增加类的任何初始化逻辑,只需要依照父类的designated initializer。 当你希望提供额外的初始化逻辑的时候你可以重载 designated initializer。你只需要重载你的直接的超类的 designated initializer 并且确认你的实现调用了超类的方法。 你一个典型的例子是你创造uiviewcontroller子类的时候重载 initwithnibname:bundle:方法。
在你希望提供你自己的初始化函数的时候,你应该遵守这三个步骤来保证正确的性:
(1)定义你的 designated initializer,确保调用了直接超类的 designated initializer
(2)重载直接超类的 designated initializer。调用你的新的 designated initializer.
(3)为新的 designated initializer 写文档
很多开发者忽略了后两步,这不仅仅是一个粗心的问题,而且这样违反了框架的规则,而且可能导致不确定的行为和bug。 让我们看看正确的实现的例子:
@implementation zocnewsviewcontroller - (id)initwithnews:(zocnews *)news { // call to the immediate superclass's designated initializer self = [super initwithnibname:nil bundle:nil]; if (self) { _news = news; } return self; } // override the immediate superclass's designated initializer (重载直接父类的 designated initializer) - (id)initwithnibname:(nsstring *)nibnameornil bundle:(nsbundle *)nibbundleornil { // call the new designated initializer return [self initwithnews:nil]; } @end
你应该考虑来用这两个策略(不是互斥的):第一个是你在文档中明确哪一个初始化方法是 designated 的,但是最好你可以用编译器的指令 attribute((objc_designated_initializer)) 来标记你的意图。 用这个编译指令的时候,编译器回来帮你。如果你的新的 designate initializer 没有调用你超类的 designated initializer,上编译器会发出警告。 然而,当没有调用类的 designated initializer 的时候(并且依次提供必要的参数),并且调用其他父类中的 designated initialize 的时候,会变成一个不可用的状态。参考之前的例子,当实例化一个 zocnewsviewcontroller 展示一个新闻而那条新闻没有展示的话,就会毫无意义。这个情况下你应该只需要让其他的 designated initializer 失效,来强制调用一个非常特别的 designated initializer。通过使用另外一个编译器指令 attribute((unavailable(“invoke the designated initializer”))) 来修饰一个方法,通过这个属性,会让你在试图调用这个方法的时候产生一个编译错误。
这是之前的例子相关的实现的头文件(这里使用宏来让代码没有那么啰嗦)
@interface zocnewsviewcontroller : uiviewcontroller - (instancetype)initwithnews:(zocnews *)news zoc_designated_initializer; - (instancetype)initwithnibname:(nsstring *)nibnameornil bundle:(nsbundle *)nibbundleornil zoc_unavailable_instead(initwithnews:); - (instancetype)init zoc_unavailable_instead(initwithnews:); @end
4.instancetype
一个相关的返回类型可以明确地规定用 instancetype 关键字作为返回类型,并且它可以在一些工厂方法或者构造器方法的场景下很有用。它可以提示编译器正确地检查类型,并且更加重要的是,这同时适用于它的子类。
@interface zocperson + (instancetype)personwithname:(nsstring *)name; @end
虽然如此,根据 clang 的定义,id 可以被编译器提升到 instancetype 。在 alloc 或者 init 中,我们强烈建议对所有返回类的实例的类方法和实例方法使用 instancetype 类型。
5.初始化模式
5.1 类簇 (class cluster)
我们的经验是使用类簇可以帮助移除很多条件语句。
一个经典的例子是如果你有为 ipad 和 iphone 写的一样的 uiviewcontroller 子类,但是在不同的设备上有不同的行为。
@implementation zockintsugiphotoviewcontroller - (id)initwithphotos:(nsarray *)photos { if ([self ismemberofclass:zockintsugiphotoviewcontroller.class]) { self = nil; if ([uidevice ispad]) { self = [[zockintsugiphotoviewcontroller_ipad alloc] initwithphotos:photos]; } else { self = [[zockintsugiphotoviewcontroller_iphone alloc] initwithphotos:photos]; } return self; } return [super initwithnibname:nil bundle:nil]; } @end
之前的代码的例子展示了如何创建一个类簇。首先,[self ismemberofclass:zockintsugiphotoviewcontroller.class] 来避免在子类中重载初始化方法,来避免无限的递归。当 [[zockintsugiphotoviewcontroller alloc] initwithphotos:photos] 得到调用的时候之前的检查会变成 true 的,self = nil 是用来移除所有到 zockintsugiphotoviewcontroller 实例的引用的,它会被释放,按照这个逻辑来检查哪个类应该被初始化。 让我们假设在 iphone 上运行了这个代码, zockintsugiphotoviewcontroller_iphone 没有重载initwithphotos:,在这个情况下,当执行 self = [[zockintsugiphotoviewcontroller_iphone alloc] initwithphotos:photos]; 的时候,zockintsugiphotoviewcontroller 会被调用,并且当第一次检查的时候,这样不会让 zockintsugiphotoviewcontroller 检查会变成 false 调用return [super initwithnibname:nil bundle:nil]; ,这会让 继续初始化执行正确的初始化之前的会话。
5.2 单例
如果可能,请尽量避免使用单例而是依赖注入。 然而,如果一定要用,请使用一个线程安全的模式来创建共享的实例。 对于gcd,用 dispatch_once() 函数就可以咯。
+ (instancetype)sharedinstance { static id sharedinstance = nil; static dispatch_once_t oncetoken = 0; dispatch_once(&oncetoken, ^{ sharedinstance = [[self alloc] init]; }); return sharedinstance; }
6.属性
属性应该尽可能描述性地命名,避免缩写,并且是小写字母开头的驼峰命名
例子 :
nsstring *text;
不要这样 :
nsstring* text; nsstring * text;
你总应该用 getter 和 setter 因为:
(1)使用 setter 会遵守定义的内存管理语义(strong, weak, copy etc…) 这回定义更多相关的在arc是钱,因为它始终是相关的。举个例子,copy 每个时候你用 setter 并且传送数据的时候,它会复制数据而不用额外的操作
(2)kvo 通知(willchangevalueforkey, didchangevalueforkey) 会被自动执行
(3)更容易debug:你可以设置一个断点在属性声明上并且断点会在每次 getter / setter 方法调用的时候执行,或者你可以在自己的自定义 setter/getter 设置断点。
(4)允许在一个单独的地方为设置值添加额外的逻辑。
你应该倾向于用 getter:
(1)它是对未来的变化有扩展能力的(比如,属性是自动生成的)
(2)它允许子类化
(3)更简单的debug(比如,允许拿出一个断点在 getter 方法里面,并且看谁访问了特别的 getter
(4)它让意图更加清晰和明确:通过访问 ivar _anivar 你可以明确的访问 self->_anivar.这可能导致问题。在 block 里面访问 ivar (你捕捉并且 retain 了 sefl 即使你没有明确的看到 self 关键词)
(5)它自动产生kvo 通知
(6)在消息发送的时候增加的开销是微不足道的。
你永远不能在 init (以及其他初始化函数)里面用 getter 和 setter 方法,并且你直接访问实例变量。事实上一个子类可以重载sette或者getter并且尝试调用其他方法,访问属性的或者 ivar 的话,他们可能没有完全初始化。记住一个对象是仅仅在 init 返回的时候,才会被认为是初始化完成到一个状态了。同样在 dealloc 方法中(在 dealloc 方法中,一个对象可以在一个 不确定的状态中)这是同样需要被注意的。
当使用 setter getter 方法的时候尽量使用点符号。应该总是用点符号来访问以及设置属性
例子:
view.backgroundcolor = [uicolor orangecolor]; [uiapplication sharedapplication].delegate;
不要这样:
[view setbackgroundcolor:[uicolor orangecolor]]; uiapplication.sharedapplication.delegate;
7.属性定义
推荐按照下面的格式来定义属性
上一篇: 关于递归调用,实现树形菜单的样式