Effective Objective-C 2.0:编写高质量iOS与OS X代码的52个有效方法

更多详情


内容简介: 《Effective Objective-C 2.0:编写高质量iOS与OS X代码的52个有效方法》是世界级C++开发大师Scott Meyers亲自担当顾问编辑的“Effective Software Development Series”系列丛书中的新作,Amazon全五星评价。从语法、接口与API设计、内存管理、框架等7大方面总结和探讨了Objective-C编程中52个鲜为人知和容易被忽视的特性与陷阱。书中包含大量实用范例代码,为编写易于理解、便于维护、易于扩展和高效的Objective-C应用提供了解决方案。
全书共7章。第1章通论与Objective-C的核心概念相关的技巧;第2章讲述的技巧与面向对象语言的重要特征(对象、消息和运行期)相关;第3章介绍的技巧与接口和API设计相关;第4章讲述协议与分类相关的技巧;第5章介绍内存管理中易犯的错误以及如何避免犯这些错误;第6章介绍块与大中枢派发相关的技巧;第7章讲解使用Cocoa和Cocoa Touch系统框架时的相关技巧。

目录: 《Effective Objective-C 2.0:编写高质量iOS与OS X代码的52个有效方法》
译者序
前言
致谢
第1章 熟悉Objective-C 1
第1条:了解Objective-C语言的起源 1
第2条:在类的头文件中尽量少引入其他头文件 4
第3条:多用字面量语法,少用与之等价的方法 7
第4条:多用类型常量,少用#define预处理指令 11
第5条:用枚举表示状态、选项、状态码 14
第2章 对象、消息、运行期 21
第6条:理解“属性”这一概念 21
第7条:在对象内部尽量直接访问实例变量 28
第8条:理解“对象等同性”这一概念 30
第9条:以“类族模式”隐藏实现细节 35
第10条:在既有类中使用关联对象存放自定义数据 39
第11条:理解objc_msgSend的作用 42
第12条:理解消息转发机制 46
第13条:用“方法调配技术”调试“黑盒方法” 52
第14条:理解“类对象”的用意 56
第3章 接口与API设计 60
第15条:用前缀避免命名空间冲突 60
第16条:提供“全能初始化方法” 64
第17条:实现description方法 69
第18条:尽量使用不可变对象 73
第19条:使用清晰而协调的命名方式 78
第20条:为私有方法名加前缀 83
第21条:理解Objective-C错误模型 85
第22条:理解NSCopying协议 89
第4章 协议与分类 94
第23条:通过委托与数据源协议进行对象间通信 94
第24条:将类的实现代码分散到便于管理的数个分类之中 101
第25条:总是为第三方类的分类名称加前缀 104
第26条:勿在分类中声明属性 106
第27条:使用“class-continuation分类”隐藏实现细节 108
第28条:通过协议提供匿名对象 114
第5章 内存管理 117
第29条:理解引用计数 117
第30条:以ARC简化引用计数 122
第31条:在dealloc方法中只释放引用并解除监听 130
第32条:编写“异常安全代码”时留意内存管理问题 132
第33条:以弱引用避免保留环 134
第34条:以“自动释放池块”降低内存峰值 137
第35条:用“僵尸对象”调试内存管理问题 141
第36条:不要使用retainCount 146
第6章 块与大中枢派发 149
第37条:理解“块”这一概念 149
第38条:为常用的块类型创建typedef 154
第39条:用handler块降低代码分散程度 156
第40条:用块引用其所属对象时不要出现保留环 162
第41条:多用派发队列,少用同步锁 165
第42条:多用GCD,少用performSelector系列方法 169
第43条:掌握GCD及操作队列的使用时机 173
第44条:通过Dispatch Group机制,根据系统资源状况来执行任务 175
第45条:使用dispatch_once来执行只需运行一次的线程安全代码 179
第46条:不要使用dispatch_get_current_queue 180
第7章 系统框架 185
第47条:熟悉系统框架 185
第48条:多用块枚举,少用for循环 187
第49条:对自定义其内存管理语义的collection使用无缝桥接 193
第50条:构建缓存时选用NSCache而非NSDictionary 197
第51条:精简initialize与load的实现代码 200
第52条:别忘了NSTimer会保留其目标对象 205

译者序: 看到Effective这个词,大家一定会想到《Effective C++》、《Effective Java》等业界名著,那些书里既汇聚了多项实用技巧,又系统而深入地讲解了各种编程知识。那么这本《Effective Objective-C 2.0》是否也是如此呢?没错,它再次演绎了经典!
作为Mac OS X与iOS应用程序的开发语言,Objective-C近年来颇受关注。尤其是智能手机与平板电脑兴起之后,越来越多的开发者都将目光转向移动平台,并开始学习Objective-C。与C++、Java一样,Objective-C也有“入门易,精通难”的问题。而本书作者Matt Galloway有多年开发经验,他将工作中遇到的各种问题分成7大类52小项,逐条罗列出来。在研读过程中,你不仅可以找到具体解决办法,而且还能体会到不同解决方案之间的优劣,更为重要的是,本书深入探讨了语言里一些鲜为人知或容易被人忽视的特性,令开发者明白其微妙之处,从而在实际工作中避开这些陷阱。书中每条心得几乎都给出了相当实用的范例代码,读者可直接将其运用在实际编程中,也可按照需要稍加改编,并举一反三,类推出更多相关技巧来。
除了讲解Objective-C语言本身外,书里还讲了与其密不可分的各种框架,相信你阅读完之后,会更深入地了解这门语言,同时编写易于理解、易于维护、易于扩展的高效应用程序应该也不再是难事了。在“从入门到精通”的过程中,本书定是你的良师益友。
本书由爱飞翔翻译,舒亚林和张军也参与了部分翻译工作,译文最后由爱飞翔统一整理。翻译过程中,得到机械工业出版社华章公司诸位编辑与工作人员的帮助,在此深表谢意。
由于时间仓促,译者水平有限,错误与疏漏之处敬请读者批评指正。大家可访问网页http://agilemobidev.com/eastarlee/book/effective-objective-c留言,亦可发电子邮件至eastarstormlee@gmail.com。
爱飞翔

前言: 许多人认为Objective-C这门语言芜杂、笨拙、别扭,但笔者却看到其雅致、灵活、美观的一面。然而,为了领略这些优点,大家不仅要掌握基础知识,还要理解语言中的特性、陷阱及繁难之处。本书正是要讲述这些内容。
关于本书
本书假定读者已经熟悉了Objective-C的语法,所以不再赘述。笔者要讲的是怎样完全发挥这门语言的优势,以编写出良好的代码。由于其源自Smalltalk,所以Objective-C是一门相当动态的语言。在其他语言中,许多工作都由编译器来完成;而在Objective-C中,则要于“运行期”(runtime)执行。于是,在测试环境下能正常运行的函数到了工作环境中,也许就会因为处理了无效数据而不能正确执行。避免此类问题的最佳方案当然是一开始就把代码写好。
严格地说,许多话题与Objective-C的核心部分并无关联。本书要谈到系统库中的技术,例如libdispatch库的“大中枢派发”(Grand Central Dispatch)等。因为当前所说的Objective-C开发就是指开发Mac OS X或iOS应用程序,所以,书中也要提及Foundation框架中的许多类,而不仅仅是其基类NSObject。不论开发Mac OS X程序还是iOS程序,都无疑会用到系统框架,前者所用的各框架统称为Cocoa,后者则叫Cocoa Touch。
随着iOS的兴起,许多开发者都涌入Objective-C开发阵营。有的程序员初学编程,有的具备Java或C++基础,还有的则是网页开发者出身。为了能高效运用Objective-C,无论是哪种情况,你都必须花时间研究这门语言,从而写出执行迅速、便于维护、不易出错的代码来。
尽管这本书只用6个月就写好了,但是其酝酿过程却长达数年。笔者某天心血来潮,买了个iPod Touch,等到第一版SDK发布之后,就决定开发个程序玩玩。我做的第一个“应用程序”叫“Subnet Calc”,其下载量比预想中要多。于是我确信,以后要和这个美妙的语言结缘了。从此就一直研究Objective-C,并定期在自己的网站www.galloway.me.uk上发表博文。我对该语言的内部工作原理,诸如“块”(block)、“自动引用计数”(Auto Reference Count,ARC)等特别感兴趣。于是,在有机会写作一本讲Objective-C的书时,自然就当仁不让了。
为使本书物尽其用,笔者建议大家跳读,直接翻到最感兴趣或与当前工作有关的章节来看。可以分开阅读每条技巧,也可以按其中所引用的条目跳至其他话题,互相参照。相关技巧归并成章,读者可根据各章标题快速找到谈及某个语言特性的数条技巧。
目标读者
本书面向那些有志于深入研究Objective-C的开发者,帮助其编写便于维护、执行迅速且不易出错的代码。如果你目前还不是Objective-C程序员,但是会用Java或C++这样面向对象的语言,那么仍可阅读此书。在这种情况下,应先了解Objective-C的语法。
本书主要内容
本书不打算讲Objective-C语言的基础知识,在许多教材和参考资料中都能找到那些内容。本书要讲的是如何高效运用这门语言。书中内容分为若干条目,每条都是一小块易于理解的文字。这些条目按其所谈话题组织为如下各章。
第1章:熟悉Objective-C
通论该语言的核心概念。
第2章:对象、消息、运行期
对象之间能够关联与交互,这是面向对象语言的重要特征。本章讲述这些特征,并深入研究代码在运行期的行为。
第3章:接口与API设计
很少有那种写完就不再复用的代码。即使代码不向更多人公开,也依然有可能用在自己的多个项目中。本章讲解如何编写与Objective-C搭配得宜的类。
第4章:协议与分类
协议与分类是两个需要掌握的重要语言特性。若运用得当,则可令代码易读、易维护且少出错。本章将帮助读者精通这两个概念。
第5章:内存管理
Objective-C语言以引用计数来管理内存,这令许多初学者纠结,要是用过以“垃圾收集器”(garbage collector)来管理内存的语言,那么更会如此。“自动引用计数”机制缓解了此问题,不过使用时有很多重要的注意事项,以确保对象模型正确,不致内存泄漏。本章提醒读者注意内存管理中易犯的错误。
第6章:块与大中枢派发
苹果公司引入了“块”这一概念,其语法类似于C语言扩展中的“闭包”(closure)。在Objective-C语言中,我们通常用块来实现一些原来需要很多样板代码才能完成的事情,块还能实现“代码分离”(code separation)。“大中枢派发”(Grand Central Dispatch,GCD)提供了一套用于多线程环境的简单接口。“块”可视为GCD的任务,根据系统资源状况,这些任务也许能并发执行。本章将教会读者如何充分运用这两项核心技术。
第7章:系统框架
大家通常会用Objective-C来开发Mac OS X或iOS程序。在这两种情况下都有一套完整的系统框架可供使用,前者名为Cocoa,后者名为Cocoa Touch。本章将总览这些框架,并深入研究其中某些类。
致  谢
在问到是否愿意写一本Objective-C的书时,我立刻兴奋起来。读过了Effective系列其他书后,我意识到要想写好这本Objective-C书籍可真是个挑战。然而在众人协助之下,这本书终于和大家见面了。
书中好些灵感都源自许多专述Objective-C的精彩博文。笔者要感谢博文作者Mike Ash、Matt Gallagher及“bbum”等人。多年来,这些博客帮助我更深地理解了Objective-C语言。在编撰本书时,NSHipster及Mattt Thompson所写的优秀文章也启迪了我的思路。还要感谢苹果公司提供了极为有用的开发文档。
在供职于MX Telecom期间,得良师益友之助,我学到了许多知识,若没有这段经历,恐怕就无法完成此书了。感谢Matthew Hodgson,令我有机会以一套成熟的C++代码库为基础,开发出公司首个iOS应用程序,在该项目中学到的本领为我参与后续项目打下了基础。
感谢历年来保持联系的各位同仁。大家时而切磋技艺,时而把酒言欢,这对我写作本书来说都是种帮助。
与培生集团旗下团队的合作相当愉快。Trina MacDonald、Olivia Basegio、Scott Meyers及Chris Zahn都在需要时给我以帮助与鼓励。诸位为我提供了专心写书的工具,并回答了必要的问题。
笔者同技术编辑合作得也非常融洽,你们给了我莫大的帮助。仰赖严格的审校,方能使本书内容臻于完美。诸位在检查书稿时认真细致的态度,也令人称赞。
最后我要说,此书能问世,爱妻Helen的理解与支持必不可少。准备动笔那天,我们的第一个孩子降生了,真正开始写作是在几天之后。Helen与Rosie伴我顺利写完这本书,你们俩真棒!

媒体评论: 第1章  熟悉Objective-C  Objective-C通过一套全新语法,在C语言基础上添加了面向对象特性。Objective-C的语法中频繁使用方括号,而且不吝于写出极长的方法名,这通常令许多人觉得此语言较为冗长。其实这样写出来的代码十分易读,只是C++或Java程序员不太能适应。  Objective-C语言学起来很快,但有很多微妙细节需注意,而且还有许多容易为人所忽视的特性。另一方面,有些开发者并未完全理解或是容易滥用某些特性,导致写出来的代码难于维护且不易调试。本章讲解基础知识,后续各章谈论语言及其相关框架中的各个特定话题。  第1条:了解Objective-C语言的起源  Objective-C与C++、Java等面向对象语言类似,不过很多方面有所差别。若是用过另一种面向对象语言,那么就能理解Objective-C所用的许多范式与模板了。然而语法上也许会显得陌生,因为该语言使用“消息结构”(messagingstructure)而非“函数调用”(functioncalling)。Objective-C语言由Smalltalk演化而来,后者是消息型语言的鼻祖。消息与函数调用之间的区别看上去就像这样:  //Messaging(Objective-C)  Object*obj=[Objectnew];  [objperformWith:parameter1and:parameter2];  //Functioncalling(C++)  Object*obj=newObject;  obj->perform(parameter1,parameter2);  关键区别在于:使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。如果范例代码中调用的函数是多态的,那么在运行时就要按照“虚方法表”(virtualtable)来查出到底应该执行哪个函数实现。而采用消息结构的语言,不论是否多态,总是在运行时才会去查找所要执行的方法。实际上,编译器甚至不关心接收消息的对象是何种类型。接收消息的对象问题也要在运行时处理,其过程叫做“动态绑定”(dynamicbinding),第11条会详述其细节。  Objective-C的重要工作都由“运行期组件”(runtimecomponent)而非编译器来完成。使用Objective-C的面向对象特性所需的全部数据结构及函数都在运行期组件里面。举例来说,运行期组件中含有全部内存管理方法。运行期组件本质上就是一种与开发者所编代码相链接的“动态库”(dynamiclibrary),其代码能把开发者编写的所有程序粘合起来。这样的话,只需更新运行期组件,即可提升应用程序性能。而那种许多工作都在“编译期”(compiletime)完成的语言,若想获得类似的性能提升,则要重新编译应用程序代码。  Objective-C是C的“超集”(superset),所以C语言中的所有功能在编写Objective-C代码时依然适用。因此,必须同时掌握C与Objective-C这两门语言的核心概念,方能写出高效的Objective-C代码来。其中尤为重要的是要理解C语言的内存模型(memorymodel),这有助于理解Objective-C的内存模型及其“引用计数”(referencecounting)机制的工作原理。若要理解内存模型,则需明白:Objective-C语言中的指针是用来指示对象的。想要声明一个变量,令其指代某个对象,可用如下语法:  NSString*someString=@“Thestring”;  这种语法基本上是照搬C语言的,它声明了一个名为someString的变量,其类型是NSString*。也就是说,此变量为指向NSString的指针。所有Objective-C语言的对象都必须这样声明,因为对象所占内存总是分配在“堆空间”(heapspace)中,而绝不会分配在“栈”(stack)上。不能在栈中分配Objective-C对象:  NSStringstackString;  //error:interfacetypecannotbestaticallyallocated  someString变量指向分配在堆里的某块内存,其中含有一个NSString对象。也就是说,如果再创建一个变量,令其指向同一地址,那么并不拷贝该对象,只是这两个变量会同时指向此对象:  NSString*someString=@“Thestring”;  NSString*anotherString=someString;  只有一个NSString实例,然而有两个变量指向此实例。两个变量都是NSString*型,这说明当前“栈帧”(stackframe)里分配了两块内存,每块内存的大小都能容下一枚指针(在32位架构的计算机上是4字节,64位计算机上是8字节)。这两块内存里的值都一样,就是NSString实例的内存地址。


书摘: 第1章
熟悉Objective-C
Objective-C通过一套全新语法,在C语言基础上添加了面向对象特性。Objective-C的语法中频繁使用方括号,而且不吝于写出极长的方法名,这通常令许多人觉得此语言较为冗长。其实这样写出来的代码十分易读,只是C++或Java程序员不太能适应。
Objective-C语言学起来很快,但有很多微妙细节需注意,而且还有许多容易为人所忽视的特性。另一方面,有些开发者并未完全理解或是容易滥用某些特性,导致写出来的代码难于维护且不易调试。本章讲解基础知识,后续各章谈论语言及其相关框架中的各个特定话题。
第1条:了解Objective-C语言的起源
Objective-C与C++、Java等面向对象语言类似,不过很多方面有所差别。若是用过另一种面向对象语言,那么就能理解Objective-C所用的许多范式与模板了。然而语法上也许会显得陌生,因为该语言使用“消息结构”(messaging structure)而非“函数调用”(function calling)。Objective-C语言由Smalltalk演化而来,后者是消息型语言的鼻祖。消息与函数调用之间的区别看上去就像这样:
// Messaging (Objective-C)
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];
// Function calling (C++)
Object *obj = new Object;
obj->perform(parameter1, parameter2);
关键区别在于:使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。如果范例代码中调用的函数是多态的,那么在运行时就要按照“虚方法表”(virtual table)来查出到底应该执行哪个函数实现。而采用消息结构的语言,不论是否多态,总是在运行时才会去查找所要执行的方法。实际上,编译器甚至不关心接收消息的对象是何种类型。接收消息的对象问题也要在运行时处理,其过程叫做“动态绑定”(dynamic binding),第11条会详述其细节。
Objective-C的重要工作都由“运行期组件”(runtime component)而非编译器来完成。使用Objective-C的面向对象特性所需的全部数据结构及函数都在运行期组件里面。举例来说,运行期组件中含有全部内存管理方法。运行期组件本质上就是一种与开发者所编代码相链接的“动态库”(dynamic library),其代码能把开发者编写的所有程序粘合起来。这样的话,只需更新运行期组件,即可提升应用程序性能。而那种许多工作都在“编译期”(compile time)完成的语言,若想获得类似的性能提升,则要重新编译应用程序代码。
Objective-C是C的“超集”(superset),所以C语言中的所有功能在编写Objective-C代码时依然适用。因此,必须同时掌握C与Objective-C这两门语言的核心概念,方能写出高效的Objective-C代码来。其中尤为重要的是要理解C语言的内存模型(memory model),这有助于理解Objective-C的内存模型及其“引用计数”(reference counting)机制的工作原理。若要理解内存模型,则需明白:Objective-C语言中的指针是用来指示对象的。想要声明一个变量,令其指代某个对象,可用如下语法:
NSString *someString = @"The string";
这种语法基本上是照搬C语言的,它声明了一个名为someString的变量,其类型是NSString*。也就是说,此变量为指向NSString的指针。所有Objective-C语言的对象都必须这样声明,因为对象所占内存总是分配在“堆空间”(heap space)中,而绝不会分配在“栈”(stack)上。不能在栈中分配Objective-C对象:
NSString stackString;
// error: interface type cannot be statically allocated
someString变量指向分配在堆里的某块内存,其中含有一个NSString对象。也就是说,如果再创建一个变量,令其指向同一地址,那么并不拷贝该对象,只是这两个变量会同时指向此对象:
NSString *someString = @"The string";
NSString *anotherString = someString;
只有一个NSString实例,然而有两个变量指向此实例。两个变量都是NSString*型,这说明当前“栈帧”(stack frame)里分配了两块内存,每块内存的大小都能容下一枚指针(在32位架构的计算机上是4字节,64位计算机上是8字节)。这两块内存里的值都一样,就是NSString实例的内存地址。
图1-1描述了此时的内存布局。存放在NSString实例中的数据含有代表字符串实际内容的字节。
图1-1 此内存布局图演示了一个分配在堆中的NSString实例,有两个分配在栈上的指针指向该实例
分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理。
Objective-C将堆内存管理抽象出来了。不需要用malloc及free来分配或释放对象所占内存。Objective-C运行期环境把这部分工作抽象为一套内存管理架构,名叫“引用计数”(参见第29条)。
在Objective-C代码中,有时会遇到定义里不含*的变量,它们可能会使用“栈空间”(stack space)。这些变量所保存的不是Objective-C对象。比如CoreGraphics框架中的CGRect就是个例子:
CGRect frame;
frame.origin.x = 0.0f;
frame.origin.y = 10.0f;
frame.size.width = 100.0f;
frame.size.height = 150.0f;
CGRect是C结构体,其定义是:
struct CGRect {
CGPoint origin;
CGSize size;
};
typedef struct CGRect CGRect;
整个系统框架都在使用这种结构体,因为如果改用Objective-C对象来做的话,性能会受影响。与创建结构体相比,创建对象还需要额外开销,例如分配及释放堆内存等。如果只需保存int、float、double、char等“非对象类型”(nonobject type),那么通常使用CGRect这种结构体就可以了。
在着手编写Objective-C代码之前,建议读者先看看C语言教程,以熟悉其语法。若是还没熟悉C语言就直接进入Objective-C的话,那么某些语法也许会令你困惑。
要点
Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
理解C语言的核心概念有助于写好Objective-C程序。尤其要掌握内存模型与指针。
第2条:在类的头文件中尽量少引入其他头文件
与C和C++一样,Objective-C也使用“头文件”(header file)与“实现文件”(implementation file)来区隔代码。用Objective-C语言编写“类”(class)的标准方式为:以类名做文件名,分别创建两个文件,头文件后缀用.h,实现文件后缀用.m。创建好一个类之后,其代码看上去如下所示:
// EOCPerson.h
#import
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@end
// EOCPerson.m
#import "EOCPerson.h"
@implementation EOCPerson
// Implementation of methods
@end
用Objective-C语言编写任何类几乎都需要引入Foundation.h。如果不在该类本身引入这个文件的话,那么就要引入与其超类所属框架相对应的“基本头文件”(base header file)。例如,在创建iOS应用程序时,通常会继承UIViewController类。而这些子类的头文件需要引入UIKit.h。
现在看来,EOCPerson类还好。其头文件引入了整个Foundation框架,不过这并没有问题。如果此类继承自Foundation框架中的某个类,那么EOCPerson类的使用者(consumer)可能会用到其基类中的许多内容。继承自UIViewController的那些类也是如此,其使用者可能会用到UIKit中的大部分内容。
过段时间,你可能又创建了一个名为EOCEmployer的新类,然后可能觉得每个EOCPerson实例都应该有一个EOCEmployer。于是,直接为其添加一项属性:
// EOCPerson.h
#import
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
然而这么做有个问题,就是在编译引入了EOCPerson.h的文件时,EOCEmployer类并不可见。不便强迫开发者在引入EOCPerson.h时必须一并引入EOCEmployer.h,所以,常见的办法是在EOCPerson.h中加入下面这行:
#import "EOCEmployer.h"
这种办法可行,但是不够优雅。在编译一个使用了EOCPerson类的文件时,不需要知道EOCEmployer类的全部细节,只需要知道有一个类名叫EOCEmployer就好。所幸有个办法能把这一情况告诉编译器:
@class EOCEmployer;
这叫做“向前声明”(forward declaring)该类。现在EOCPerson的头文件变成了这样:
// EOCPerson.h
#import
@class EOCEmployer;
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
EOCPerson类的实现文件则需引入EOCEmployer类的头文件,因为若要使用后者,则必须知道其所有接口细节。于是,实现文件就是:
// EOCPerson.m
#import "EOCPerson.h"
#import "EOCEmployer.h"
@implementation EOCPerson
// Implementation of methods
@end
将引入头文件的时机尽量延后,只在确有需要时才引入,这样就可以减少类的使用者所需引入的头文件数量。假设本例把EOCEmployer.h引入到EOCPerson.h,那么只要引入EOCPerson.h,就会一并引入EOCEmployer.h的所有内容。此过程若持续下去,则要引入许多根本用不到的内容,这当然会增加编译时间。
向前声明也解决了两个类互相引用的问题。假设要为EOCEmployer类加入新增及删除雇员的方法,那么其头文件中会加入下述定义:
- (void)addEmployee:(EOCPerson*)person;
- (void)removeEmployee:(EOCPerson*)person;
此时,若要编译EOCEmployer,则编译器必须知道EOCPerson这个类,而要编译EOCPerson,则又必须知道EOCEmployer。如果在各自头文件中引入对方的头文件,则会导致“循环引用”(chicken-and-egg situation)。当解析其中一个头文件时,编译器会发现它引入了另一个头文件,而那个头文件又回过头来引用第一个头文件。使用#import而非#include指令虽然不会导致死循环,但却这意味着两个类里有一个无法被正确编译。如果不信的话,读者可以自己试试。
但是有时候必须要在头文件中引入其他头文件。如果你写的类继承自某个超类,则必须引入定义那个超类的头文件。同理,如果要声明你写的类遵从某个协议(protocol),那么该协议必须有完整定义,且不能使用向前声明。向前声明只能告诉编译器有某个协议,而此时编译器却要知道该协议中定义的方法。
例如,要从图形类中继承一个矩形类,且令其遵循绘制协议:
// EOCRectangle.h
#import "EOCShape.h"
#import "EOCDrawable.h"
@interface EOCRectangle : EOCShape
@property (nonatomic, assign) float width;
@property (nonatomic, assign) float height;
@end
第二条#import是难免的。鉴于此,最好是把协议单独放在一个头文件中。要是把EOCDrawable协议放在了某个大的头文件里,那么只要引入此协议,就必定会引入那个头文件中的全部内容,如此一来,就像上面说的那样,会产生相互依赖问题,而且还会增加编译时间。
然而有些协议,例如“委托协议”(delegate protocol,参见第23条),就不用单独写一个头文件了。在那种情况下,协议只有与接受协议委托的类放在一起定义才有意义。此时最好能在实现文件中声明此类实现了该委托协议,并把这段实现代码放在“class-continuation分类”(class-continuation category,参见第27条)里。这样的话,只要在实现文件中引入包含委托协议的头文件即可,而不需将其放在公共头文件(public header file)里。
每次在头文件中引入其他头文件之前,都要先问问自己这样做是否确有必要。如果可以用向前声明取代引入,那么就不要引入。若因为要实现属性、实例变量或者要遵循协议而必须引入头文件,则应尽量将其移至“class-continuation分类”中(参见第27条)。这样做不仅可以缩减编译时间,而且还能降低彼此依赖程度。若是依赖关系过于复杂,则会给维护带来麻烦,而且,如果只想把代码的某个部分开放为公共API的话,太复杂的依赖关系也会出问题。
要点
除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。
有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
第3条:多用字面量语法,少用与之等价的方法
编写Objective-C程序时,总会用到某几个类,它们属于Foundation框架。虽然从技术上来说,不用Foundation框架也能写出Objective-C代码,但实际上却经常要用到此框架。这几个类是NSString、NSNumber、NSArray、NSDictionary。从类名上即可看出各自所表示的数据结构。
Objective-C以语法繁杂而著称。事实上的确是这样。不过,从Objective-C 1.0起,有一种非常简单的方式能创建NSString对象。这就是“字符串字面量”(string literal),其语法如下:
NSString *someString = @"Effective Objective-C 2.0";
如果不用这种语法的话,就要以常见的alloc及init方法来分配并初始化NSString对象了。在版本较新的编译器中,也能用这种字面量语法来声明NSNumber、NSArray、NSDictionary类的实例。使用字面量语法(literal syntax)可以缩减源代码长度,使其更为易读。
字面数值
有时需要把整数、浮点数、布尔值封入Objective-C对象中。这种情况下可以用NSNumber类,该类可处理多种类型的数值。若是不用字面量,那么就需要按下述方式创建实例:
NSNumber *someNumber = [NSNumber numberWithInt:1];
上面这行代码创建了一个数字,将其值设为整数1。然而使用字面量能令代码更为整洁:
NSNumber *someNumber = @1;
大家可以看到,字面量语法更为精简。不过它还有很多好处。能够以NSNumber实例表示的所有数据类型都可使用该语法。例如:
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *doubleNumber = @3.14159;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
字面量语法也适用于下述表达式:
int x = 5;
float y = 6.32f;
NSNumber *expressionNumber = @(x * y);
以字面量来表示数值十分有用。这样做可以令NSNumber对象变得整洁,因为声明中只包含数值,而没有多余的语法成分。
字面量数组
数组是常用的数据结构。如果不使用字面量语法,那么就要这样来创建数组:
NSArray *animals =
[NSArray arrayWithObjects:@"cat", @"dog",
@"mouse", @"badger", nil];
而使用字面量语法来创建则是:
NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
上面这种做法不仅简单,而且还利于操作数组。数组的常见操作就是取某个下标所对应的对象,这用字面量来做更为容易。如果不用字面量,那么通常会用“objectAtIndex:”方法:
NSString *dog = [animals objectAtIndex:1];
若使用字面量,则是:
NSString *dog = animals[1];
这也叫做“取下标”操作(subscripting),与使用字面量语法的其他情况一样,这种方式也更为简洁、更易理解,而且与其他语言中依下标来访问数组元素时所用的语法类似。
不过,用字面量语法创建数组时要注意,若数组元素对象中有nil,则会抛出异常,因为字面量语法实际上只是一种“语法糖”(syntactic sugar),其效果等于是先创建了一个数组,然后把方括号内的所有对象都加到这个数组中。抛出的异常会是这样:
*** Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '***
-[__NSPlaceholderArray initWithObjects:count:]: attempt to
insert nil object from objects[0]'
在改用字面量语法来创建数组时就会遇到这个问题。下面这段代码分别以两种语法创建数组:
id object1 = /* ... */;
id object2 = /* ... */;
id object3 = /* ... */;
NSArray *arrayA = [NSArray array WithObjects:
object1, object2, object3, nil];
NSArray *arrayB = @[object1, object2, object3];
大家想想:如果object1与object3都指向了有效的Objective-C对象,而object2是nil,那么会出现什么情况呢?按字面量语法创建数组arrayB时会抛出异常。arrayA虽然能创建出来,但是其中却只含有object1一个对象。原因在于,“arrayWithObjects:”方法会依次处理各个参数,直到发现nil为止,由于object2是nil,所以该方法会提前结束。
这个微妙的差别表明,使用字面量语法更为安全。抛出异常令应用程序终止执行,这比创建好数组之后才发现元素个数少了要好。向数组中插入nil通常说明程序有错,而通过异常可以更快地发现这个错误。
字面量字典
“字典”(Dictionary)是一种映射型数据结构,可向其中添加键值对。与数组一样,Objective-C代码也经常用到字典。其创建方式如下:
NSDictionary *personData =
[NSDictionarydictionaryWithObjectsAndKeys:
@"Matt", @"firstName",
@"Galloway", @"lastName",
[NSNumber numberWithInt:28], @"age",
nil];
这样写令人困惑,因为其顺序是<对象>,<键>,<对象>,<键>。这与通常理解的顺序相反,我们一般认为是把“键”映射到“对象”。因此,这种写法不容易读懂。如果改用字面量语法,就清晰多了:
NSDictionary *personData =
@{@"firstName" : @"Matt",
@"lastName" : @"Galloway",
@"age" : @28};
上面这种写法更简明,而且键出现在对象之前,理解起来较顺畅。此范例代码还说明了使用字面量数值的好处。字典中的对象和键必须都是Objective-C对象,所以不能把整数28直接放进去,而要将其封装在NSNumber实例中才行。使用字面量语法很容易就能做到这一点,只需给数字前加一个@字符即可。
与数组一样,用字面量语法创建字典时也有个问题,那就是一旦有值为nil,便会抛出异常。不过基于同样的原因,这也是个好事。假如在创建字典时不小心用了空值对象,那么“dictionaryWithObjectsAndKeys:”方法就会在首个nil之前停下,并抛出异常,这有助于查错。
字典也可以像数组那样用字面量语法访问。按照特定键访问其值的传统做法是:
NSString *lastName = [personData objectForKey:@"lastName"];
与之等效的字面量语法则是:
NSString *lastName = personData[@"lastName"];
这样写也省去了冗赘的语法,令此行代码简单易读。
可变数组与字典
通过取下标操作,可以访问数组中某个下标或字典中某个键所对应的元素。如果数组与字典对象是可变的(mutable),那么也能通过下标修改其中的元素值。修改可变数组与字典内容的标准做法是:
[mutableArray replaceObjectAtIndex:1 withObject:@"dog"];
[mutableDictionary setObject:@"Galloway"forKey:@"lastName"];
若换用取下标操作来写,则是:
mutableArray[1] = @"dog";
mutableDictionary[@"lastName"] = @"Galloway";
局限性
字面量语法有个小小的限制,就是除了字符串以外,所创建出来的对象必须属于Foundation框架才行。如果自定义了这些类的子类,则无法用字面量语法创建其对象。要想创建自定义子类的实例,必须采用“非字面量语法”(nonliteral syntax)。然而,由于NSArray、NSDictionary、NSNumber都是业已定型的“子族”(class cluster,参见第9条),因此很少有人会从其中自定义子类,真要那样做也比较麻烦。而且一般来说,标准的实现已经很好了,无须再改动。创建字符串时可以使用自定义的子类,然而必须要修改编译器的选项才行。除非你明白这样做的后果,否则不鼓励使用此选项,用NSString就足够了。
使用字面量语法创建出来的字符串、数组、字典对象都是不可变的(immutable)。若想要可变版本的对象,则需复制一份:
NSMutableArray *mutable = [@[@1, @2, @3, @4, @5]mutableCopy];
这么做会多调用一个方法,而且还要再创建一个对象,不过使用字面量语法所带来的好处还是多于上述缺点的。
要点
应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。
第4条:多用类型常量,少用#define预处理指令
编写代码时经常要定义常量。例如,要写一个UI视图类,此视图显示出来之后就播放动画,然后消失。你可能想把播放动画的时间提取为常量。掌握了Objective-C与其C语言基础的人,也许会用这种方法来做:
#define ANIMATION_DURATION 0.3
上述预处理指令会把源代码中的ANIMATION_DURATION字符串替换为0.3。这可能就是你想要的效果,不过这样定义出来的常量没有类型信息。“持续”(duration)这个词看上去应该与时间有关,但是代码中又未明确指出。此外,预处理过程会把碰到的所有ANIMATION_DURATION一律替换成0.3,这样的话,假设此指令声明在某个头文件中,那么所有引入了这个头文件的代码,其ANIMATION_DURATION都会被替换。
要想解决此问题,应该设法利用编译器的某些特性才对。有个办法比用预处理指令来定义常量更好。比方说,下面这行代码就定义了一个类型为NSTimeInterval的常量:
static const NSTimeInterval kAnimationDuration = 0.3;
请注意,用此方式定义的常量包含类型信息,其好处是清楚地描述了常量的含义。由此可知该常量类型为NSTimeInterval,这有助于为其编写开发文档。如果要定义许多常量,那么这种方式能令稍后阅读代码的人更易理解其意图。
还要注意常量名称。常用的命名法是:若常量局限于某“编译单元”(translation unit,也就是“实现文件”,implementation file)之内,则在前面加字母k;若常量在类之外可见,则通常以类名为前缀。第19条详解了命名习惯(naming convention)。
定义常量的位置很重要。我们总喜欢在头文件里声明预处理指令,这样做真的很糟糕,当常量名称有可能互相冲突时更是如此。例如,ANIMATION_DURATION这个常量名就不该用在头文件中,因为所有引入了这份头文件的其他文件中都会出现这个名字。其实就连用static const定义的那个常量也不应出现在头文件里。因为Objective-C没有“名称空间”(namespace)这一概念,所以那样做等于声明了一个名叫kAnimationDuration的全局变量。此名称应该加上前缀,以表明其所属的类,例如可改为EOCViewClassAnimationDuration。本书第19条中深入讲解了一套清晰的命名方案。
若不打算公开某个常量,则应将其定义在使用该常量的实现文件里。比方说,要开发一个使用UIKit框架的iOS应用程序,其UIView子类中含有表示动画播放时间的常量,那么可以这样写:
// EOCAnimatedView.h
#import
@interface EOCAnimatedView : UIView
- (void)animate;
@end
// EOCAnimatedView.m
#import "EOCAnimatedView.h"
static const NSTimeInterval kAnimationDuration = 0.3;
@implementation EOCAnimatedView
- (void)animate {
[UIViewanimateWithDuration:kAnimationDuration
animations:^(){
// Perform animations
}];
}
@end
变量一定要同时用static与const来声明。如果试图修改由const修饰符所声明的变量,那么编译器就会报错。在本例中,我们正是希望这样:因为动画播放时长为定值,所以不应修改。而static修饰符则意味着该变量仅在定义此变量的编译单元中可见。编译器每收到一个编译单元,就会输出一份“目标文件”(object file)。在Objective-C的语境下,“编译单元”一词通常指每个类的实现文件(以.m为后缀名)。因此,在上述范例代码中声明的kAnimationDuration变量,其作用域仅限于由EOCAnimatedView.m所生成的目标文件中。假如声明此变量时不加static,则编译器会为它创建一个“外部符号”(external symbol)。此时若是另一个编译单元中也声明了同名变量,那么编译器就抛出一条错误消息:
duplicate symbol _kAnimationDuration in:
EOCAnimatedView.o
EOCOtherView.o
实际上,如果一个变量既声明为static,又声明为const,那么编译器根本不会创建符号,而是会像#define预处理指令一样,把所有遇到的变量都替换为常值。不过还是要记住:用这种方式定义的常量带有类型信息。
有时候需要对外公开某个常量。比方说,你可能要在类代码中调用NSNotificationCenter以通知他人。用一个对象来派发通知,令其他欲接收通知的对象向该对象注册,这样就能实现此功能了。派发通知时,需要使用字符串来表示此项通知的名称,而这个名字就可以声明为一个外界可见的常值变量(constant variable)。这样的话,注册者无须知道实际字符串值,只需以常值变量来注册自己想要接收的通知即可。
此类常量需放在“全局符号表”(global symbol table)中,以便可以在定义该常量的编译单元之外使用。因此,其定义方式与上例演示的static const有所不同。应该这样来定义:
// In the header file
extern NSString *const EOCStringConstant;
// In the implementation file
NSString *const EOCStringConstant = @"VALUE";
这个常量在头文件中“声明”,且在实现文件中“定义”。注意const修饰符在常量类型中的位置。常量定义应从右至左解读,所以在本例中,EOCStringConstant就是“一个常量,而这个常量是指针,指向NSString对象”。这与需求相符:我们不希望有人改变此指针常量,使其指向另一个NSString对象。
编译器看到头文件中的extern关键字,就能明白如何在引入此头文件的代码中处理该常量了。这个关键字是要告诉编译器,在全局符号表中将会有一个名叫EOCStringConstant的符号。也就是说,编译器无须查看其定义,即允许代码使用此常量。因为它知道,当链接成二进制文件之后,肯定能找到这个常量。
此类常量必须要定义,而且只能定义一次。通常将其定义在与声明该常量的头文件相关的实现文件里。由实现文件生成目标文件时,编译器会在“数据段”(data section)为字符串分配存储空间。链接器会把此目标文件与其他目标文件相链接,以生成最终的二进制文件。凡是用到EOCStringConstant这个全局符号的地方,链接器都能将其解析。
因为符号要放在全局符号表里,所以命名常量时需谨慎。例如,某应用程序中有个处理登录操作的类,在登录完成后会发出通知。派发通知所用的代码如下:
// EOCLoginManager.h
#import
extern NSString *const EOCLoginManagerDidLoginNotification;
@interface EOCLoginManager : NSObject
- (void)login;
@end
// EOCLoginManager.m
#import "EOCLoginManager.h"
NSString *const EOCLoginManagerDidLoginNotification =
@"EOCLoginManagerDidLoginNotification";
@implementation EOCLoginManager
- (void)login {
// Perform login asynchronously, then call 'p_didLogin'.
}
- (void)p_didLogin {
[[NSNotificationCenter defaultCenter]
postNotificationName:EOCLoginManagerDidLoginNotification
object:nil];
}
@end
注意常量的名字。为避免名称冲突,最好是用与之相关的类名做前缀。系统框架中一般都这样做。例如UIKit就按照这种方式来声明用作通知名称的全局常量。其中有类似UIApplicationDidEnterBackgroundNotification与UIApplicationWillEnterForegroundNotification这样的常量名。
其他类型的常量也是如此。假如要把前例中EOCAnimatedView类里的动画播放时长对外公布,那么可以这样声明:
// EOCAnimatedView.h
extern const NSTimeInterval EOCAnimatedViewAnimationDuration;
// EOCAnimatedView.m
const NSTimeInterval EOCAnimatedViewAnimationDuration = 0.3;
这样定义常量要优于使用#define预处理指令,因为编译器会确保常量值不变。一旦在EOCAnimatedView.m中定义好,即可随处使用。而采用预处理指令所定义的常量可能会无意中遭人修改,从而导致应用程序各个部分所使用的值互不相同。
总之,勿使用预处理指令定义常量,而应该借助编译器来确保常量正确,比方说可以在实现文件中用static const来声明常量,也可以声明一些全局常量。
要点
不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
在实现文件中使用static const来定义“只在编译单元内可见的常量”(translation-unit-specific constant)。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。
第5条:用枚举表示状态、选项、状态码
由于Objective-C基于C语言,所以C语言有的功能它都有。其中之一就是枚举类型:enum。系统框架中频繁用到此类型,然而开发者容易忽视它。在以一系列常量来表示错误状态码或可组合的选项时,极宜使用枚举为其命名。由于C++11标准扩充了枚举的特性,所以最新版系统框架使用了“强类型”(strong type)的枚举。没错,Objective-C也能得益于C++11标准。
枚举只是一种常量命名方式。某个对象所经历的各种状态就可以定义为一个简单的枚举集(enumeration set)。比如说,可以用下列枚举表示“套接字连接”(socket connection)的状态:
enum EOCConnectionState {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
由于每种状态都用一个便于理解的值来表示,所以这样写出来的代码更易读懂。编译器会为枚举分配一个独有的编号,从0开始,每个枚举递增1。实现枚举所用的数据类型取决于编译器,不过其二进制位(bit)的个数必须能完全表示下枚举编号才行。在前例中,由于最大编号是2,所以使用1个字节的char类型即可。
然而定义枚举变量的方式却不太简洁,要依如下语法编写:
enum EOCConnectionState state = EOCConnectionStateDisconnected;
若是每次不用敲入enum而只需写EOCConnectionState就好了。要想这样做,则需使用typedef关键字重新定义枚举类型:
enum EOCConnectionState {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
typedef enum EOCConnectionState EOCConnectionState;
现在可以用简写的EOCConnectionState来代替完整的enum EOCConnectionState了:
EOCConnectionState state = EOCConnectionStateDisconnected;
C++11标准修订了枚举的某些特性。其中一项改动是:可以指明用何种“底层数据类型”(underlying type)来保存枚举类型的变量。这样做的好处是,可以向前声明枚举变量了。若不指定底层数据类型,则无法向前声明枚举类型,因为编译器不清楚底层数据类型的大小,所以在用到此枚举类型时,也就不知道究竟该给变量分配多少空间。
指定底层数据类型所用的语法是:
enum EOCConnectionStateConnectionState : NSInteger { /* ... */ };
上面这行代码确保枚举的底层数据类型是NSInteger。也可以在向前声明时指定底层数据类型:
enum EOCConnectionStateConnectionState : NSInteger;
还可以不使用编译器所分配的序号,而是手工指定某个枚举成员所对应的值。语法如下:
enum EOCConnectionStateConnectionState {
EOCConnectionStateDisconnected = 1,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
上述代码把EOCConnectionStateDisconnected的值设为1,而不使用编译器所分配的0。如前所述,接下来几个枚举的值都会在上一个的基础上递增1。比如说,EOCConnectionState-Connected的值就是3。
还有一种情况应该使用枚举类型,那就是定义选项的时候。若这些选项可以彼此组合,则更应如此。只要枚举定义得对,各选项之间就可通过“按位或操作符”(bitwise OR operator)来组合。例如,iOS UI框架中有如下枚举类型,用来表示某个视图应该如何在水平或垂直方向上调整大小:
enum UIViewAutoresizing {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth= 1 << 1,
UIViewAutoresizingFlexibleRightMargin= 1 << 2,
UIViewAutoresizingFlexibleTopMargin= 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5,
}
每个选项均可启用或禁用,使用上述方式来定义枚举值即可保证这一点,因为在每个枚举值所对应的二进制表示中,只有1个二进制位的值是1。用“按位或操作符”可组合多个选项,例如:UIViewAutoResizingFlexibleWidth