编写高质量代码:改善Objective-C程序的61个建议(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-15 19:53:27

点击下载

作者:刘一道

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

编写高质量代码:改善Objective-C程序的61个建议

编写高质量代码:改善Objective-C程序的61个建议试读:

前言

如何写出高质量的代码?

我一直在思考,如何才能编写出高质量、优秀的代码,我也在不停地探寻,希望找出类似于武侠小说中所说的武功秘籍,在编写代码一途可以帮助大家走“捷径”从而达到事半功倍的效果。《道德经》第四十八章中说“为学者日益,为道者日损。损之有损,以至于无为。无为而不为”。这句话是说,治学上,不要过于追求外在的经验知识,否则经验知识越积累增多,就会越僵化臃肿。要学会透过直观体悟把握事物未分化时的状态或者内索自身虚静,洞悉其内在的道化真谛,从而简之再简。这些也就是我们现在说的“大道至简”。

治学如此,写代码更是如此。在程序员写代码的职业生涯中,前5年,他看到的只是一行一行的代码,他会为自己洋洋洒洒写成的代码而陶醉;5年之后,就不是单纯地写代码了,而是在做一件艺术品,此时的程序员就像雕刻家一样,在刻下每一刀之前,都需纵观全局,细细揣摩,落刀如有神,一气呵成。故此,写出优秀的高质量代码,需要像唐僧西天取经一样,踏踏实实,用平常心闯过一关又一关,如此,写出高质量的代码自然就是水到渠成的事了。写代码时切忌心态浮躁,急功近利。

本书适合哪些读者

本书不是一本介绍“Objective-C”代码如何编写的入门级的书籍。故此,如果你只想初步了解一下“Objective-C”开发,而不想做深入研究的话,那么本书就不适合你了。

本书主要面向专业从事Objective-C开发或者想转向“Objective-C”开发的研究人员,帮助其编写便于维护、执行迅速且不易出错的代码。如果你是“Objective-C”开发技术大咖,翻阅本书,对你来说可能会有些浪费时间,故此也请你一瞥而过!

本书主要适合如下读者:

对软件开发,特别是对Objective-C开发有兴趣的人。

想成为一名专职的软件开发人员的人。

想进一步提高自己“Objective-C”技术水平的在校学生。

开设相关专业课程的大专院校的师生。

你该如何阅读本书

本书共9章,从内容上可以分3部分。大家可以根据自身状况,选择性跳读,翻阅自己最感兴趣或与当前工作相关的章节来读。下面把这几章简单归类评述,为你进行选择性跳读时提供参考:

第一部分(第1~5章):主要围绕如何写出高质量代码提出一些建议。这些建议一部分来源于苹果每年举行的一些开发大会,主要是针对Objective-C语法、性能与功能改进进行的阐述和解惑;另一部分来自于一线开发者平时工作中的点点滴滴的感受。作为笔者,我不过是把这些点滴像串珍珠一样把它们串起来,形成一个实用的、具有整体性的建议。

第1章 作为本书的首章,我感觉唠唠“Objective-C的那些事儿”很有必要。希望通过这一章的唠叨能让你有所收获。

第2章 主讲数据类型、集合和控制语句。《道德经》第五十二章中有言“天下有始,以为天下母。既得其母,以知其子;既知其子,复守其母。”掌握了基本的开发语言,就可以“知其子”—程序;反过来,通过开发程序,又可以“复守其母”—开发语言,即通过开发程序可以进一步巩固、加深对开发语言的理解。在Objective-C中,构建语言的基本单元—值和集合,更是重中之重。本章将围绕这些构建开发语言的“基石”从不同角度加以阐述。

第3章 主讲内存管理。内存管理曾经是程序员的噩梦,特别是在面向过程开发的过程中。虽然,在纯面向对象的开发语言中,例如C#和Java等开发语言,有了自动内存垃圾回收的机制,但依赖这种自动内存垃圾回收机制的代价往往很高,容易造成程序性能的耗损,进而容易产生难以突破的“性能劲瓶”。因此,在编写高性能的应用程序,特别是服务器程序时,不得不依赖C、Pascal和C++等非纯面向对象的语言来完成。现在,众多的Android手机或者平板上遇到的App莫名其妙崩溃的现象,在很大程度上与Android底层的内存回收处理不当有很大的关系。

在本书定稿之时,虽然,Objective-C已经具有了“垃圾自动回收”的机制,但洞悉其内存的管理机制,写出高质量的代码,还是至关重要的。

第4章主讲设计与声明。《庄子·齐物论》中曰:“昔者庄周梦为蝴蝶,……不知周之梦为蝴蝶与?蝴蝶之梦为周与?”意思是说,庄子梦见自己变成蝴蝶,醒来后发现自己是庄周。庄子分不清自己是梦中变成蝴蝶,还是蝴蝶梦中变成庄子了。在Objective-C中,类与对象的关系,就有些类似庄子和蝴蝶的关系,从一定的角度来看,类就是对象,而从另外一个角度来看,对象又是类。本章就以庄周梦蝶做引子,来谈谈设计和声明。

第5章 众所周知,在面向对象的设计中,对于一个类,完成其定义,要包含类名、实例变量及方法的定义和声明,但这最多也只能算是完成20%的工作,其余80%的工作主要在于其实现。如何把类的实现写好,很考验一个人的功底。尽可能利用开发语言本身一些特性,是实现类的一个比较有效的途径。本章就结合Objective-C的特性,来介绍在类的实现中一些可利用的做法。

第二部分(第6~8章),相对于第一部分,本部分更多关注一些“习以为常”的理念,结合Objective-C语言,进行一番新的解读。这几章里面,很多东西你也许早已经知道,但一些理念如何融入自己的代码和设计中,却是更高的要求。正如巴菲特的理财理念,很多人都能知道,且能背诵得滚瓜烂熟,而在实际操作上,结果却大相径庭,其主要原因是,很多“理念”自己不过是记住罢了,并未已融入自己的血液和骨髓里,在实际操作上,仍旧沉溺于“旧习”。

第6章 主讲继承与面向对象的设计。随着这几年面向对象编程(OOP)越来越盛行,在编程领域,现在几乎是面向对象编程语言的天下了。继承,作为面向对象编程的三大特征(继承、多态和封装)之一,起着核心的作用。但不同面向对象的编程语言实现的方式,有着比较大的差异性。本章从纵向和横向两个维度,来阐述“继承”在Objective-C开发语言中的多面性,使你娴熟掌握“继承”在Objective-C开发语言中的“有所为而有所不为”。

第7章 主讲设计模式与Cocoa编程。设计模式是一种设计模板,用于解决在特定环境中反复出现的一般性问题。它是一种抽象工具,在架构、工程和软件开发领域相当有用。本章将介绍什么是设计模式,解释为什么设计模式在面向对象的设计中非常重要,并讨论一个设计模式的实例。

第8章 主讲定制init...和dealloc。在Objective-C 中,没有new和delete 这两个关键字,但存在着alloc和init...,它们承担着与new类似的功能,前者用于处理内存的分配,后者用于在分配的内存中进行数据的初始化;与delete类似的是dealloc。掌握init...,对于写出高性能的应用是至关重要的。

第三部分(第9章),在本书将近定稿时,恰遇到Swift的新版本推出,为了解决开发者在混用Objective-C和Swift过程中遇到的一些疑难点,故此添加了本部分。如果对 Swift“不感冒”,那么本部分你就没有阅读的必要了。

第9章 主讲Objective-C和Swift的兼容性。Swift是苹果公司在WWDC 2014新发布的一门编程语言,用来撰写OS X和iOS应用程序,但是Swift的推出,使很多原先很熟练使用 Objective-C的码农产生了一定程度的“惊惶”:担忧苹果公司放弃对Objective-C的支持,不得不花费一定的时间和精力来学习一门新的开发语言—Swift。Xcode 6或者更高的版本支持二者的相互兼容。也就是说,在Objective-C文件中可以使用Swift编写的代码,也可以在Swift文件中使用Objective-C编写的代码,但其相互兼容性是有条件的。本章就来讲解二者的兼容性问题。

本书没有采取循序渐进的方式。故此,读者可以根据“兴趣”选择自己喜欢的章节来阅读,这是最有效的读书方式,也是笔者推崇的读书方式。

勘误和支持

除封面署名外,参加本书编写工作的还有孙振江、陈连增、边伟、郭合苍、郑军、吴景峰、杨珍民、王文朝、崔少闯、韦闪雷、刘红娇、王洁、于雪龙、孔琴。由于笔者的水平有限,编写时间仓促,书中难免会出现一些错误或者不准确的地方,恳请读者批评指正。

书中的不足之处,还望各位读者多提意见,欢迎发送邮件至邮箱yifeilang@aliyun. com,期待能够得到你们的真挚反馈。

感恩致谢

本书之所以能出版,多亏华章公司的编辑孙海亮先生多次催促,不断地给予我鼓励。期间华章群一些网友的鼓励,也使我受益匪浅,在此向他们表示感谢。最后,要感谢的就是我亲爱的读者,感谢你拿起这本书,你的认可,就是我的最大的快乐。刘一道于北京

第1章 让自己习惯Objective-C

听说有本书叫《明朝那些事儿》,很多人喜欢,但我没有看过。我这人有些“老帽儿”,对一些时髦的东西不是很感兴趣。我看的都是那些经过千百年“浪沙冲洗”沉淀下来的书籍(技术书籍除外)。作为本书的首章,我感觉聊 一些“Objective-C的那些事儿”很有必要,希望读者能有所收获。

建议1:视Objective-C为一门动态语言

Objective-C,是美国人布莱德·确斯(Brad Cox,见图1-1)于1980年年初发明的一种程序设计语言,其与同时代的C++一样,都是在C的基础上加入面向对象特性扩充而成的。C++如日中天,红火已有30年之久,而Objective-C到2010年才被人注意,并逐渐红火起来。造成这种结果的原因跟其版权为苹果独自拥有和苹果自我封闭性、不作为性有很大的关系。这也是Objective-C在语法上跟其他语言(如C++、Pascal、Java和C#)等相比有些不足的原因。可喜的是,苹果公司于2010年起,在每年的苹果年度发布大会上都会针对Objective-C语言的语法发布新的改良版本。

虽然Objective-C和 C++都是在C的基础上加入面向对象特性扩充而成的程序设计语言,但二者实现的机制差异很大。Objective-C基于动态运行时类型,而C++基于静态类型。也就是说,用Objective-C编写的程序不能直接编译成可令机器读懂的机器语言(二进制编码),而是在程序运行时,通过运行时(Runtime)把程序转译成可令机器读懂的机器语言;用C++编写的程序,编译时就直接编译成了可令机器读懂的机器语言。这也就是为什么把Objective-C视为一门动态开发语言的原因。

从原理上来讲,目前常用的Java和C#等程序开发语言都是动态开发语言,只不过Java用的是虚拟机JVM(Java Virtual Machine),如图1-2所示,C#用的是公共语言运行时CLR(Common Language Runtime)。与此相对,C++可视为静态开发语言。

Objective-C作为一门动态开发语言,会尽可能地将编译和链接时要做的事情推迟到运行时。只要有可能,Objective-C 总是使用动态的方式来解决问题。这意味着 Objective-C 语言不仅需要一个编译环境,同时也需要一个运行时系统来执行编译好的代码。

而运行时(Runtime)正是扮演着这样的角色。在一定程度上,运行时系统类似于Objective-C 语言的操作系统,Objective-C 就基于该系统来工作。因此,运行时系统好比Objective-C的灵魂,很多东西都是在这个基础上出现的。很多开发人员常常对运行时系统充满疑惑,而恰恰这是一个非常重要的概念。可以这么问:“如果让你实现(设计)一个计算机语言,你会如何下手?” 很少程序员这么思考过。但是这么一问,就会强迫你从更高层次来思考以前的问题了。

要想理解“Objective-C是一门动态开发语言”,就不得不理解开发语言的三个不同层次。(1)传统的面向过程的语言开发。例如C语言,实现C语言编译器很简单,只要按照语法规则实现一个LALR语法分析器就可以了。编译器优化是非常难的,不在本节讨论的范围内。这里实现了编译器中最基础和原始的目标之一,就是把一份代码里的函数名称转化成一个相对内存地址,把调用这个函数的语句转换成一个跳转指令。在程序开始运行时,调用语句可以正确跳转到对应的函数地址。这样很好,也很直白,但是太死板了。(2)改进的开发面向对象的语言。相对面向过程的语言来说,面向对象的语言更加灵活了。例如C++,C++在C的基础上增加了类的部分。但这到底意味着什么呢?再写它的编译器要如何考虑呢?其实,就是让编译器多绕个弯,在严格的C编译器上增加一层类处理的机制,把一个函数限制在它处在的类环境里,每次请求一个函数调用,先找到它的对象,其类型、返回值、参数等确定后再跳转到需要的函数。这样很多程序增加了灵活性,同样一个函数调用会根据请求参数和类的环境返回完全不同的结果。增加类机制后,就模拟了现实世界的抽象模式,不同的对象有不同的属性和方法。同样的方法,不同的类有不同的行为!这里大家就可以看到作为一个编译器开发者都做了哪些进一步的思考。虽然面相对象的语言有所改进,但还是死板,故此仍称C++是静态语言。(3)动态开发语言。希望更加灵活,于是完全把上面那个类的实现部分抽象出来,做成一套完整运行阶段的检测环境,形成动态开发语言。这次再写编译器甚至保留部分代码里的sytax名称,名称错误检测,运行时系统环境注册所有全局的类、函数、变量等信息,可以无限地为这个层增加必要的功能。调用函数时,会先从这个运行时系统环境里检测所有可能的参数再做jmp跳转。这就是运行时系统。编译器开发起来比上面更加绕弯,但是这个层极大地增加了程序的灵活性。例如,当调用一个函数时,前两种语言很有可能一个跳到了一个非法地址导致程序崩溃,但是在这个层次里面,运行时系统过滤掉了这些可能性。 这就是动态开发语言更加强壮的原因,因为编译器和运行时系统环境开发人员已经帮你处理了这些问题。

好了,上面说了这么多,再返回来看Objective-C的这些语句:

id obj=self;

if ([obj respondsToSelector:@selector(function1:)) {

}

if ([obj isKindOfClass:[NSArray class]] ) {

}

if ([obj conformsToProtocol:@protocol(myProtocol)]) {

}

if ([[obj class] isSubclassOfClass:[NSArray class]]) {

}

[obj someNonExistFunction];

看似很简单的语句,但是为了让语言实现这个能力,语言开发者要付出很多努力来实现运行时系统环境。这里运行时系统环境处理了弱类型及函数存在检查工作。运行时系统会检测注册列表里是否存在对应的函数,类型是否正确,最后确定正确的函数地址,再进行保存寄存器状态、压栈、函数调用等实际的操作。

id knife=[Knife grateKnife];

NSArray *monsterList=[NSArray array];

[monsterList makeObjectsPerformSelector:@selector(killMonster:)

withObject:knife];

用C和C++完成这个功能还是比较麻烦的,但是动态语言处理却非常简单,并且这些语句让Objective-C语言更加直观。

在Objective-C中,针对对象的函数调用将不再是普通的函数调用:

[obj function1With:var1];

这样的函数调用将被运行时系统环境转换成:

objc_msgSend(target,@selector(function1With:),var1);

Objective-C的运行时系统环境是开源的,这里可以进行简单介绍,可以看到objc_ msgSend由汇编语言实现,甚至不必阅读代码,只需查看注释就可以了解。运行时系统环境在函数调用前做了比较全面的安全检查,已确保动态语言函数调用不会导致程序崩溃。对于希望深入学习的朋友可以自行下载Objective-C的运行时系统源代码来阅读,进行更深入的了解。

/********************************************************************

* id objc_msgSend(id self,

* SEL op,

* ...)

*

* a1是消息接收者,a2是选择器

********************************************************************/

ENTRY objc_msgSend

#检查接收者是否空

teq a1, #0

moveq a2, #0

bxeq lr

# 保存寄存器(对象),通过在缓存里查找,来加载接收者(对象)类

stmfd sp!, {a4,v1-v3}

ldr v1, [a1, #ISA]

#寄存器(对象)非空,缓存里可查找到

CacheLookup a2, LMsgSendCacheMiss

#缓存碰撞(imp in ip) —准备转发,恢复寄存器和调用

teq v1, v1 /* 设置 nonstret (eq) */

ldmfd sp!, {a4,v1-v3}

bx ip

#缓存漏掉:查找方法列表

LMsgSendCacheMiss:

ldmfd sp!, {a4,v1-v3}

b _objc_msgSend_uncached

LMsgSendExit:

END_ENTRY objc_msgSend

.text

.align 2

_objc_msgSend_uncached:

#压入堆栈帧

stmfd sp!, {a1-a4,r7,lr}

add r7, sp, #16

SAVE_VFP

# 加载类和选择器

ldr a1, [a1, #ISA] /* class = receiver->isa */

# MOVE a2, a2 /*选择器已经在a2 */

#做查找

MI_CALL_EXTERNAL(__class_lookupMethodAndLoadCache)

MOVE ip, a1

# 准备转发,挤出堆栈帧并且调用imp

teq v1, v1 /* 设置nonstret (eq) */

RESTORE_VFP

ldmfd sp!, {a1-a4,r7,lr}

bx ip

现在说一下动态开发语言的负面影响,其负面影响可以归纳为两方面。

1.执行效率问题“静态开发语言执行效率要比动态开发语言高”,这句没错。因为一部分CPU计算损耗在了运行时系统过程中,而从上面的汇编代码也可以看出大概损耗在哪些地方。而静态语言生成的机器指令更简洁。正因为知道这个原因,所以开发语言的人付出很大一部分努力来保持运行时系统的小巧。所以,Objective-C是C的超集加上一个小巧的运行时系统环境。但是,换句话说,从算法角度考虑,这点复杂度是可忽略的。

2.安全性问题

动态语言由于运行时系统环境的需求,会保留一些源码级别的程序结构。这样就给破解带来了方便之门。一个现成的说明就是Java,大家都知道Java运行在jre上面,这就是典型的运行时例子。它的执行文件.class全部可以反编译回近似源代码。所以,这里的额外提示就是,如果需要写和安全有关的代码,离Objective-C远点,直接用C。

简单理解:“Runtime is everything between your each function call.”

但是大家要明白,提到运行时系统并不只是因为它带来了这些简便的语言特性,而是这些简单的语言特性在实际运用中,需要从完全不同的角度考虑。

要点(1)Objective-C 是动态语言,C++是静态语言。(2)静态语言执行效率和安全性要比动态语言高,但其简便性没有动态语言高。(3)运行时(Runtime)环境可处理弱类型、函数存在检查工作,会检测注册列表里是否存在对应的函数,类型是否正确,最后确定正确的函数地址,再进行保存寄存器状态、压栈、函数调用等实际操作,确保了Objective-C的灵活性。

建议2:在头文件中尽量减少其他头文件的引用

在面向对象开发语言中,如C++、C#、Java等语言中,对于类的描述,通常划分为头文件和源文件。头文件用于描述类的声明和可公开部分,而源文件用于描述类的方法或函数的具体实现,这也体现了面向对象语言的“封闭性”和“高内聚低耦合”的特性。而对于基于面向对象而设计的Objective-C也不例外,类分为头文件(.h)和源文件(.m)。

在OOP编程中有两个技术用于描述类与类或对象与对象之间的关系:一个是继承;另一个是复合。在Objective-C中,当一个类需要引用另一个类,即建立复合关系时,需要在类的头文件(.h)中,通过“#import ”修饰符来建立被引用类的指针。例如Car.h:

// Car.h

#import

@interface Car:NSObject

{

Tire *tires[4];

Engine *engine;

}

- (void) setEngine: (Engine *) newEngine;

- (Engine *) engine;

- (void) setTire: (Tire *) tire atIndex: (int) index;

- (Tire *) tireAtIndex: (int) index;

- (void) print;

@end // Car

在这里先省略类Car的源文件(.m)。对于上面的代码,如果直接这么编译,编译器会报错,提示它不知道Tire和Engine是什么。为了使上面的代码能编译通过,在上面代码中就不得不添加对类Tire和Engine的头文件(.h)的引用,即通过关键字“#import”来建立起它们之间的复合关系。

要建立正确的复合关系,正确的代码写法如下:

// Car.h

#import

#import "Tire.h"

#import "Engine.h"

@interface Car:NSObject

{

Tire *tires[4];

Engine *engine;

}

- (void) setEngine: (Engine *) newEngine;

- (Engine *) engine;

- (void) setTire: (Tire *) tire atIndex: (int) index;

- (Tire *) tireAtIndex: (int) index;

- (void) print;

@end // Car

现在,上面的代码虽然能正确编译了,但从“代码的高品质高安全”角度来看,在使用“#import”建立类之间的复合关系时,也暴露了所引用类Tire和Engine的实体变量和方法,与只需知道有一个类名叫Tire和Engine的初衷有些违背。在解决问题的同时,也带来了代码的安全性问题。

那么如何解决上面的问题呢?可以使用关键字@class来告诉编译器:这是一个类,所以只需要通过指针来引用它。它并不需要知道关于这个类的更多信息,只要了解它是通过指针引用即可,减少由依赖关系引起的重新编译所产生的影响。

对于上面的代码,通过@Class即可来建立对于类Tire和Engine的引用,具体写法如下:

// Car.h

#import

@class Tire

@class Engine

@interface Car:NSObject

{ Tire *tires[4]; Engine *engine;

}

- (void) setEngine: (Engine *) newEngine;

- (Engine *) engine;

- (void) setTire: (Tire *) tire atIndex: (int) index;

- (Tire *) tireAtIndex: (int) index;

- (void) print;

@end // Car

上面介绍了使用“#import”和“@class”在“依赖关系”方面所产生的影响。同时二者在编译效率方面也存在巨大的差异。假如,有100个头文件,都用“#import”引用了同一个头文件,或者这些文件是依次引用的,如A–>B、B–>C、C–>D这样的引用关系。当最开始的那个头文件有变化时,后面所有引用它的类都需要重新编译,如果自己的类有很多的话,这将耗费大量的时间,而使用@class则不会。

对于初学者,最容易犯 “类循环依赖”错误。所谓的“类循环依赖”,也就是说,两个类相互引用对方。在本条款最初的Car.h头文件中,通过“#import ”引用了Tire.h头文件,假如在Tire.h头文件里引用Car.h头文件,即如下:

// Tire.h

#import

#import "Tire.h"

上面的代码进行编译时会出现编译错误,如果使用@class在两个类的头文件中相互声明,则不会有编译错误出现。虽然使用@class不会出现编译错误,但还是尽量避免这种“类循环依赖”的出现,因为这样容易造成类之间“高耦合”现象的产生,给以后代码的维护和管理带来很大的麻烦。“#import”并非一无是处。既然 “#import”与“@class”相比有很多不足,那么是否可以用“@class”来完全代替“#import”?不可以,在一个头文件(.h)中包含多个类的声明定义时,要与该头文件声明的多个类建立复合关系,比较好的方式是,采用关键字“#import”来建立复合关系。

例如,下面是头文件PersonType.h的定义:

//PersonType.h

#import

// Person类的声明定义

@interface Person:NSObject

{

NSInteger *sexType;

}

@property (nonatomic copy) NSString *firstname;

@property (nonatomic copy) NSString *lastname;

- (void) setSexType: : (int) index;

@end

// man类的声明定义

@interface man:Person

end

//woman类的声明定义

@interface woman:Person

end;

要与上面头文件PersonType.h中所声明的类建立复合关系,这个时候就不得不用关键字“#import”。使用“#import”建立复合关系,会把所引用的头文件(.h)的所有类进行预编译,这样就会消耗很长时间。是否是有一种更好的方式来处理这个问题,请参阅“条款 9 尽量使用模块方式与多类建立复合关系”。

一般来说,关键字“@class”放在头文件中只是为了在头文件中引用这个类,把这个类作为一个类型来用。这就要求引用的头文件(.h)名与类的名称一致,且在类头文件(.h)只包含该类的声明定义的情况下,才可以使用关键字“@class”来建立复合关系。同时,在实现这个类的接口的类源文件(.m)中,如果需要引用这个类的实体变量或方法等,还需要通过“#import”把在“@class”中声明的类引用进来。例如下面的类a引用类Rectangle的示例:

a.h

@class Rectangle;

@interface A : NSObject {

...

}

a.m

#import Rectangle

@implementation A

...

上面的种种介绍,其核心的目的就是为了“降低类与类之间的耦合度”。也就是说,降低类与类之间的复合关系黏性度。

在自己设计类的时候,除了“#import”和“@class”之外,有没有一种更好的方式?有的,一种是通过使用模块方式与多类建立复合关系,详细情况请参阅建议6;另一种是通过使用“协议”的方式来实现。

在Objective-C中,实际上,协议就是一个穿了“马甲”的接口。通过使用“协议”来降低类与类之间的耦合度。例如,把协议单独写在一个文件中,注意,千万不要把协议写入到一个大的头文件中,这样做,凡是只要引入此协议,就必定会引入头文件中的全部内容,如此一来,类与类之间的耦合度就会大大增加,不利于代码的管理及程序的稳定性和安全性,为以后的工作带来很大的麻烦。

故此,在自己设计类的时候,首先要明白,通过使用“#import”和“@class”,每次引入其他的头文件是否有必要。如果要引用的类和该类所在的文件同名,最好采用“@ class”方式来引入。如果引用的类所处的文件有多个类或者多个其他的定义,最好采用“模块方式”来针对性引入自己所需要的类,详细情况请参与建议6。不管采取哪种方式,降低类与类的耦合度,降低不同文件代码之间过度的黏合性是首要的目的。代码的依赖关系过于复杂则会失去代码的重用性,给维护代码带来很大的麻烦,同时,使编译的应用的稳定性和高效性也大打折扣。

要点(1)在头文件(.h)中,关键字“@class”,只是为了在头文件中引用这个类,把这个类作为一个类型来用,这就要求引用的头文件(.h)名与类的名称一致。(2)在头文件(.h)中使用“@class ”,在源文件(.m)中使用“#import”,不但可以减少不必要的编译时间,降低类之间的耦合度,而且还可以避免循环引用。(3)在设计类时,尽量多采用协议,避免#import过多,引进不必要的部分。(4)如果头文件(.h)中有多个类的定义,尽量采用模块方式,只针对性引进所需要的类。

建议3:尽量使用const、enum来替换预处理#define

#define 定义了一个宏,在编译开始之前就会被替换。const只是对变量进行修饰,当试图去修改该变量时,编译器会报错。在一些场合里你只能用 #define,而不能用 const。理论上来说,const 不仅在运行时需要占用空间,而且还需要一个内存的引用;但从时间上来说,这是无关紧要的,编译器可能会对其进行优化。

const在编译和调试的时候比 #define 更友好。在大多数情况下,当你决定用哪一个时,这是你应该考虑的一个非常重要的点。

想象一下,下面这样一个应该使用 #define 而不是 const 的场景:如果想在大量的 .c 文件中使用一个常量,只需要使用 #define 放在头文件中;而使用 const,则需要在 .c 文件和头文件中都进行定义,如下所示。

// in a C file

const int MY_INT_CONST = 12345;

// in a header

extern const int MY_INT_CONST;

MY_INT_CONST ,在任何 C 文件中都不能当作一个静态变量或全局作用域使用,除非它已被定义。然而,对于一个整型常量,你可以使用枚举(enum)。事实上,这就是Apple一直在做的事情。它(enum)兼有 #define和 const 的所有优点,但是只能用在整型常量上。

// In a header

enum

{

MY_INT_CONST = 12345,

};

哪一个更高效或更安全呢?#define 在理论上来说更高效,但就像之前说的那样,在现代的编译器上,它们可能没什么不同。#define 会更安全,因为当试图赋值给它时,总会出现一个编译器错误。

因此,相对字符串字面量或数字,更推荐适用常量。应使用static方式声明常量,而非使用#define的方式来定义宏。

恰当用法如下所示:

static NSString * const NYTAboutViewControllerCompanyName = @"The New York Times Company";

static const CGFloat NYTImageThumbnailHeight = 50.0;

不当用法如下所示:

#define CompanyName @"The New York Times Company"

#define thumbnailHeight 2

对于整型类型,代替#define比较好的方法是使用enum,在使用enum时,推荐使用最新的fixed underlying type规范的NS_ENUM和NS_OPTIONS宏,因为它们是基于C语言的枚举,保留了C语言的简洁和简单的特色。这些宏能明确指定枚举类型、大小和选项,改善在Xcode中的代码质量。此外,在旧的编译器中,这种语法声明能正确编译通过,在新的编译器中也可明解基础类型的类型信息。

下面,使用NS_ENUM宏定义枚举。该组值是互斥的,代码如下:

typedef NS_ENUM(NSInteger, UITableViewCellStyle) {

UITableViewCellStyleDefault,

UITableViewCellStyleValue1,

UITableViewCellStyleValue2,

UITableViewCellStyleSubtitle

};

在本例中,在命名UITableViewCellStyle的NSInteger类型时,通过使用NS_ENUM宏使界定双方的名称和枚举类型更容易了。

在下面的代码中,通过使用NS_OPTIONS宏定义了一组可以组合在一起的位掩码值,实现方式如下:

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {

UIViewAutoresizingNone = 0,

UIViewAutoresizingFlexibleLeftMargin = 1 << 0,

UIViewAutoresizingFlexibleWidth = 1 << 1,

UIViewAutoresizingFlexibleRightMargin = 1 << 2,

UIViewAutoresizingFlexibleTopMargin = 1 << 3,

UIViewAutoresizingFlexibleHeight = 1 << 4,

UIViewAutoresizingFlexibleBottomMargin = 1 << 5

};

像枚举一样,NS_OPTIONS宏定义了一个名称和一个类型。然而,对于选项的类型通常应该是NSUInteger 。

在实际编码中,如何使用枚举宏来更换enum,比如这样一个enum定义:

enum {

UITableViewCellStyleDefault,

UITableViewCellStyleValue1,

UITableViewCellStyleValue2,

UITableViewCellStyleSubtitle

};

typedef NSInteger UITableViewCellStyle;

用NS_ENUM宏来实现上面的定义,其语法如下:

typedef NS_ENUM(NSInteger, UITableViewCellStyle) {

UITableViewCellStyleDefault,

UITableViewCellStyleValue1,

UITableViewCellStyleValue2,

UITableViewCellStyleSubtitle

};

在实际开发中,经常会使用enum来定义一个位掩码,如下面一个用enum来定义位掩码的示例:

enum {

UIViewAutoresizingNone = 0,

UIViewAutoresizingFlexibleLeftMargin = 1 << 0,

UIViewAutoresizingFlexibleWidth = 1 << 1,

UIViewAutoresizingFlexibleRightMargin = 1 << 2,

UIViewAutoresizingFlexibleTopMargin = 1 << 3,

UIViewAutoresizingFlexibleHeight = 1 << 4,

UIViewAutoresizingFlexibleBottomMargin = 1 << 5

};

typedef NSUInteger UIViewAutoresizing;

对于上面的通过用enum定义位掩码的示例,可使用NS_OPTIONS宏实现如下:

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {

UIViewAutoresizingNone = 0,

UIViewAutoresizingFlexibleLeftMargin = 1 << 0,

UIViewAutoresizingFlexibleWidth = 1 << 1,

UIViewAutoresizingFlexibleRightMargin = 1 << 2,

UIViewAutoresizingFlexibleTopMargin = 1 << 3,

UIViewAutoresizingFlexibleHeight = 1 << 4,

UIViewAutoresizingFlexibleBottomMargin = 1 << 5

};

或者,也可以使用现代化的Objective-C的转换器在Xcode自动进行此更改。

要点(1)尽量避免使用#define预处理命令。#define预处理命令不包含任何的类型信息,仅仅是在编译前做替换操作。它们在重复定义时不会发出警告,容易在整个程序中产生不一致的值。(2)在源文件(.m)中定义的static const类型常量因为无须全局引用,所以它们的名字不需要包含命名空间。(3)在头文件(.h)中定义的全局引用的常量,需要关联定义在源文件(.m)中的部分。因为需要被全局引用,所以它们的名字需要包含命名空间,通常是用它们的类名作为命名前缀。(4)尽量用NS_ENUM和NS_OPTIONS宏来实现枚举。

建议4:优先使用对象字面量语法而非等效方法

很多刚从其他编程语言转到Objective-C的程序员,往往一看到长长的函数名就会感到崩溃,这种语法让消息的传递像一个英语句子,虽有不足但确实大大增强了可读性。比如想初始化一个浮点数,需要这么写:

NSNumber value = [NSNumber numberWithFloat:123.45f];

从这句中能够明确地知道代码的含义,但是,是否连简单的赋值语句也要这么处理呢?在2012年的苹果年度大会上,苹果介绍了大量Objective-C的新特性之一—对象字面量(Object Literals),能够帮助iOS程序员更加高效地编写代码。在XCode 4.4版本中,这个新特性已经可以使用了。

对象字面量(Object Literals)允许方便地定义数字、数组和字典对象。这个功能类似于Java 5提供的auto boxing功能。这虽然是一个语法改进,但是对提高写代码的效率帮助很大。苹果在本次新特性中采用了折中的处理方式,针对很多基础类型采用了简写的方式,实现语法简化。简化以后,会发现在语法层面这些简化的Objective-C更像Python和Ruby等动态语言的语法了。

下面先来看看以前定义数字、数组和字典对象的方法:

NSNumber * number = [NSNumber numberWithInt:1];

NSArray * array = [NSArray arrayWithObjects:@"one", @"two", nil];

NSDictionary * dict = [NSDictionary dictionaryWithObjectsAndKeys:@"value1",

@"key1", @"value2", @"key2", nil];

是不是很烦琐?现在以上代码可以简化成以下形式,不用再在参数的最后加nil了,字典的key和value也不再是倒着先写value,再写key了:

NSNumber * number = @1;

NSArray * array = @[@"one", @"two"];

NSDictionary * dict = @{@"key1":@"value1", @"key2":@"value2"};

下面逐一介绍。

1. 数字(NSNumber)

简化前的写法:

NSNumber *value;

value = [NSNumber numberWithInt:12345];

value = [NSNumber numberWithFloat:123.45f];

value = [NSNumber numberWithDouble:123.45];

value = [NSNumber numberWithBool:YES];

简化后的写法:

NSNumber *value;

value = @12345;

value = @123.45f;

value = @123.45;

value = @YES;

装箱表达式也可以采用类似的写法:

NSNumber *piOverSixteen = [NSNumber numberWithDouble: ( M_PI / 16 )];

NSString *path = [NSString stringWithUTF8String: getenv("PATH")];

可以分别简写为:

NSNumber *piOverSixteen = @( M_PI / 16 );

NSString *path = @( getenv("PATH") );

对于字符串表达式来说,需要注意的是,表达式的值一定不能是NULL,否则会抛出异常。

2. 数组(NSArray)

对于NSArray的初始化来说,有非常多的写法,这里就不再一一罗列,直接看新的写法:

NSArray *array;

array = @[]; //空数组

array = @[ a ]; //一个对象的数组

array = @[ a, b, c ]; //多个对象的数组

非常简单,再也不用记住初始化多个对象的数组时,后面还要跟一个nil。现在看一下当声明多个对象的数组时,编译器是如何处理的。

array = @[ a, b, c ];

编译器生成的代码:

id objects[] = { a, b, c };

NSUInteger count = sizeof(objects)/ sizeof(id);

array = [NSArray arrayWithObjects:objects count:count];

好吧,编译器已经把这些简单重复的工作都做了,现在可以安心解决真正的问题了。不过有一点要注意,如果a、b、c对象有nil的话,运行时系统会抛出异常,这点和原来的处理方式不同,编码时要多加小心。

注意

数字(NSArray)和字典(NSDictionary)等类,由于能像“容器”一样容纳东西,所以,通常把这些具有容器特性的类称为容器类。

3. 字典(NSDictionary)

同样,对于字典这个数据结构来说,有很多种初始化的方式,来看新的写法:

NSDictionary *dict;

dict = @{}; //空字典

dict = @{ k1 : o1 }; //包含一个键值对的字典

dict = @{ k1 : o1, k2 : o2, k3 : o3 }; //包含多个键值对的字典

4.下标法与容器类

容器的语法简化让人不难想到,可以通过下标的方式存取数组和字典的数据。比如对于数组:

NSArray *array = @[ a, b, c ];

可以这样写:

//通过下标方式获取数组对象,替换原有写法:array objectAtIndex:i];

id obj = array[i];

//也可以直接为数组对象赋值。替换原有写法

//[array replaceObjectAtIndex:i withObject:newObj];

array[i] = newObj;

对于字典:

NSDictionary *dict = @{ k1 : o1, k2 : o2, k3 : o3 };

可以这样写:

//获取o2对象,替换原有写法:[dic objectForKey:k2];

id obj = dict[k2];

//重新为键为k2的对象赋值,替换原有写法:[dic setObject:newObj forKey:k2]

dic[k2] = newObj;

同时,自己定义的容器类只要实现了规定的下标方法,就可以采用下标的方式访问数据。要实现的方法如下。

数组类型的下标方法:

- (elementType)objectAtIndexedSubscript:(indexType)idx;

- (void)setObject:(elementType)object atIndexedSubscript:(indexType)idx;

字典类型的下标方法:

- (elementType)objectForKeyedSubscript:(keyType)key;

- (void)setObject:(elementType)object forKeyedSubscript:(keyType)key;

其中需要注意的是,indexType必须是整数,elementType和keyType必须是对象指针。

5.容器类数据结构简化的限制

采用上述写法构建的容器都是不可变的,如果需要生成可变容器,可以传递-mutable Copy消息。例如:

NSMutableArray *mutablePlanets = [@[

@"Mercury", @"Venus", @"Earth",

@"Mars", @"Jupiter", @"Saturn",

@"Uranus", @"Neptune"

] mutableCopy];

不能对常量数组直接赋值,解决办法是在类方法(void)initialize中进行赋值处理,如下:

@implementation MyClass

static NSArray *thePlanets;

+ (void)initialize {

if (self == [MyClass class]) {

thePlanets = @[

@"Mercury", @"Venus", @"Earth",@"Mars", @"Jupiter", @"Saturn",

@"Uranus", @"Neptune"

];

} }

要点(1)尽量使用对象字面量语法来创建字符串、数字、数组和字典等,使用它比使用以前的常规对象创建方法语法更为精简,同时可以避免一些常见的陷阱。(2)对象字面量语法特性是完全向下兼容,使用新特性编写出来的代码,经过编译后形成的二进制程序可以运行在之前发布的任何OS中。(3)在数字和字典中,要使用关键字和索引做下标来获取数据。(4)使用对象字面量语法时,容器类的不可是nil,否则运行时将会抛出异常。

建议5:处理隐藏的返回类型,优先选择实例类型而非id

实例类型(Instancetype)是Objective-C语言中新添加的一个返回类型,实例类型作为方法返回的实例的类型,是苹果在2013年的年度大会上宣布的。这个新添加的实例类型不仅可用来作为Objective-C方法的返回类型,且能用这个实例类型来作为向编译器的提示,提示方法返回的类型将是方法所属的类的实例。

类的实例,作为方法返回类型,宜采用关键字instancetype作为方法的返回类型,如alloc 、init和类工厂方法等。使用instancetype作为类(或者类的子类)的实例返回类型,可以大大改善Objective-C代码的类型安全。例如,考虑下面的代码:

@interface MyObject : NSObject

+ (instancetype)factoryMethodA;

+ (id)factoryMethodB;

@end

@implementation MyObject

+ (instancetype)factoryMethodA {

return [[[self class] alloc] init];

}

+ (id)factoryMethodB {

return [[[self class] alloc] init];

}

@end

void doSomething() {

NSUInteger x, y;

// Return type of +factoryMethodA is taken to be "MyObject *"

x = [[MyObject factoryMethodA] count];

// Return type of +factoryMethodB is "id"

y = [[MyObject factoryMethodB] count]; }

通过上面的代码可以看到,instancetype作为+ factoryMethodA的返回类型,也就是说,该消息的类型表达式是MyObject *。但是MyObject由于先天缺乏一个-count方法,编译器将会对此给出一个关于x行的警告:

main.m: 'MyObject'may not respond to'count'

对于这样的情况,如果把instancetype换成id作为实例的方法返回类型,也就是如上面的代码中的实例,id作为类方法 + factoryMethodB的返回类型。在编译器编译的过程中不会发出关于y行的警告。

为什么编译器没有给出警告?因为 id类型的对象可以作为任何类,并且调用的方法-count在一些类中存在,故此向编译器发出方法+factoryMethodB返回值实现了-count的信息,从而编译器没有给出警告。对于该种编写代码的方法,在无形中埋下了隐患。

为了确保instancetype工厂方法有正确的子类的行为,一定要使用[self class]分配类,而不是直接引用类名。遵循这个惯例,记住,务必要使编译器能正确地推断出子类类型。例如,依据前面的示例,考虑尝试做一个前面MyObject子类示例:

@interface MyObjectSubclass : MyObject

@end

void doSomethingElse() {

NSString *aString = [MyObjectSubclass factoryMethodA];

}

对于上述代码,编译器将会给出警告,如下面的警告:

main.m: Incompatible pointer types initializing 'NSString *'with an expression of type 'MyObjectSubclass *'

在该示例中,+factoryMethodA消息发送之后,将返回一个类型MyObjectSubclass的对象实例。编译器就能恰当地确定 + factoryMethodA的返回类型应该是子类MyObjectSubclass,而不是工厂方法中所声明的超类。

在编写代码中,通常在处理init 方法和类工厂方法时,宜用instancetype类替换 id作为返回值。在新版本Xcode 5中,虽然,编译器会自动地把alloc、init、new方法之中的id转化为instancetype类型,但对于这几种方法之外的其他方法,编译器则不会进行转化。在Objective-C的公约之中,明确地建议对于所有方法尽可能用instancetype而非id。也就是说,作为返回值,id由于其自身的缺陷,在Objective-C中会逐渐退出,由instancetype来替代。

注意

仅有在作为返回值时,宜用instancetype来替换id,而不是代替代码中所有id。与id不同,instancetype关键字仅能作为方法声明的返回类型。也就说,在某一个特定区域,instancetype可以替代id,并非所有区域都可以替代id。

例如:

@interface MyObject

- (id)myFactoryMethod;

@end

应该成为:

@interface MyObject

- (instancetype)myFactoryMethod;

@end

要点(1)instancetype仅仅用来作为Objective-C方法的返回类型。(2)使用instancetype可避免隐式转换id而造成的欺骗性编译无误通过的现象,防止程序正式运行时出现崩溃现象,可以大大改善Objective-C代码的类型安全。(3)在某一个特定区域,instancetype可以替代id,并非所有区域都可以替代id。

建议6:尽量使用模块方式与多类建立复合关系

在2013年的苹果年度大会上,苹果在Objective-C的性能改进上大的变化之一就是加入了模块(Modules)。

1.文件编译问题的存在性—编译时间过长

在了解模块(Modules)之前,需要先了解一下Objective-C的#import机制。通过使用#import,用来引用其他的头文件。

熟悉C或者C++的人可能会知道,在C和C++里是没有#import的,只有#include(虽然GCC现在为C和C++做了特殊处理,使#imoprt可以被编译),用来包含头文件。#include做的事情其实就是简单的复制、粘贴,将目标.h文件中的内容一字不落地复制到当前文件中,并替换掉这句include;而#import实质上做的事情和#include是一样的,只不过Objective-C为了避免重复引用可能带来的编译错误(这种情况在引用关系复杂的时候很可能发生,比如B和C都引用了A,D又同时引用了B和C,这样A中定义的东西就在D中被定义了两次,造成重复)而加入了#import,从而保证每个头文件只会被引用一次。仔细探究一下,#import的实现是通过对#ifndef一个标志进行判断,然后再引入#define这个标志,来避免重复引用的。

实质上,#import也是复制、粘贴,这样就带来一个问题:当引用关系很复杂或一个头文件被非常多的实现文件引用时,编译时引用所占的代码量就会大幅上升(因为被引用的头文件在各个地方都被复制了一遍)。

在编写Objective-C代码中,估计很多人已经写了一千遍或更多#import语句:

//

//AppDelegate

//

#import

按照上面所说,这意味着,对于UIKit框架,通过计算所有行的全部UIKit中的头,就会发现它相当于超过11 000行代码!在一个标准的iOS应用中,就会在大部分文件中导入UIKit,这意味着每一个文件最终变成11000行。这是不够理想的,更多的代码意味着更长的编译时间。

2.预编译头文件(Pre-compiled Headers)处理方式—不实用

理论上讲,解决这个问题可采取C语言的方式,引入预编译头文件(Pre-compiled Headers,PCH),即把公用的头文件放入预编译头文件中预先进行编译。通过在编译的预处理阶段,预先计算和缓存需要的代码;然后在真正编译工程时再将预先编译好的产物加入到所有待编译的源文件中去,来加快编译速度。比如iOS开发中Supporting Files组内的.pch文件,就是一个预编译头文件。默认情况下,它引用了UIKit和Foundation两个头文件,这是在iOS开发中基本上每个实现文件都会用到的东西。

下面是Xcode生成的stock PCH 文件,像这样:

#import

#ifndef __IPHONE_5_0

#warning "This project uses features only available in iOS SDK 5.0 and later."

#endif

#ifdef __OBJC__

#import

#import

#endif

如果开发的应用是使用iOS 5之前的SDK,那么#warning将通知它们。UIKit和Foundation 头文件是stockPCH的一部分。因为在应用程序里的每一个文件将使用Foundation,并且大部分会使用UIKit。因此,这些都被很好地添加进了预编译头文件,以便于在自己的应用程序中预先计算和缓存这些文件的编译文件。

但维护项目的预编译头文件是很棘手的。利用预编译头文件虽然可以加快编译的时间,但是这样面临的问题是,在工程中随处可用本来应该不能访问的东西,而编译器也无法准确给出错误或者警告,无形中增加了出错的可能性。

3.利用模块( Modules)来解决历史问题—事半功倍

模块(Modules),第一次在Objective-C中公共露面是在2012 LLVM开发者大会上 Apple’s Doug Gregor的一次谈话中。

模块(Modules),封装框架比以往任何时候都更加清洁。不再需要预处理逐行地用文件的所有内容替换#import指令。相反,一个模块包含了一个框架到自包含的块中,就像预编译文件预编译的方式一样提升了编译速度。并且不需要在预编译头文件中声明自己要用到哪些框架,使用模块简单地获得了速度上的提升。图1-3所示为预编译文件和模块功能在编译上的比较。图1-3 预编译文件和模块功能编译改进的比较

模块还有其他方面的优点,在下列的操作编写代码的步骤过程中,都可以感受到模块的特性。(1)在使用框架的文件中添加#import。(2)用框架写代码。(3)编译。(4)查看链接错误。(5)忘记链接的框架。(6)添加忘记的框架到项目中。(7)重新编译。

忘记链接框架式是编写程序代码中经常会犯的一个错误,利用模块就能解决这个问题。一个模块不仅告诉编译器哪些头文件组成了模块,而且还告诉编译器什么需要链接。这样就不用去手动链接框架了。虽然这是一件小事,但是能让开发变得更加简单,这就是一件好事。

4. 开启使用模块

模块的使用相当简单。对于存在的工程,第一件事情就是使这个功能生效。可以在项目的Build Settings中通过搜索Modules找到这个选项,将Enable Modules 选项设为Yes,如图1-4所示。图1-4 开启模块功能

在默认情况下,模块功能所有的新工程都是开启的,但是应该在自己所有存在的工程中都开启这个功能。在图1-4中,Link Frameworks Automatically选项,可以用来开启或者关闭自动链接框架的功能。

一旦模块(Modules)功能开启,就可以在自己代码中使用它了。要这样做,对以前用到的语法有一点小小的改动,那用@import代替#import:

@import UIKit;

@import MapKit;

@import iAd;

另外,只导入一个框架中自己需要的部分也是可以的。例如,只想要导入UIView,就可以这样写:

@import UIKit.UIView;

使用模块功能就是这么简单。技术上,自己不需要把所有的#import都换成@import,因为编译器会隐式地转换它们,但是还是建议尽可能地用新的语法来编写代码。

注意

Xcode 5.0的模块功能还不支持自己的或第三方的框架。目前Xcode 6的测试版都出来了,在很多Xcode的新版本中,都支持模块功能。

要点(1)#include和#import,其根本就是简单的复制、粘贴,将目标.h文件中的内容一字不落地复制到当前文件中,后者可以避免多次的重复引用。(2)以预编译头文件的方式,虽可缩短编译时间,但其维护棘手,不利于广泛应用。(3)模块功能,其应用不仅仅表现于编译的速度加快,同时在链接框架等方面也非常好用。(4)启动模块功能后,编译器会隐式地把所有的#import都转换成@import。

建议7:明解Objective-C++中的有所为而有所不为

苹果的Objective-C编译器允许用户在同一个源文件(.m)里自由地混合使用C++和Objective-C,混编后的语言叫作Objective-C++。有了它,你就可以在Objective-C应用程序中使用已有的C++类库。

在Objective-C++中,可以用C++代码调用方法,也可以从Objective-C调用方法。在这两种语言中对象都是指针,可以在任何地方使用。例如,C++类可以使用Objective-C对象的指针作为数据成员,Objective-C类也可以有C++对象指针做实例变量。

注意

Xcode需要源文件以“.mm”为扩展名,这样才能启动编译器的Objective-C++扩展。

下面,将一一介绍二者的联系及区别。

1. 二者的定义结构一样,但是Objcetive-C的继承是封闭的(1)定义一个C++类Hello,代码如下:

#import

class Hello {

private:

id greeting_text; // holds an NSString

public:

//方法Hello

Hello() {

greeting_text = @"Hello, world!";

}

//方法Hello

Hello(const char* initial_greeting_text) {

greeting_text = [[NSString alloc]

initWithUTF8String:initial_greeting_text];

}

//方法say_hello

void say_hello() {

printf("%s\n", [greeting_text UTF8String]);

}

};(2)定义一个Objcetive-C类Greeting,代码如下:

@interface Greeting : NSObject {

@private

Hello *hello;

}

- (id)init;

- (void)dealloc;

- (void)sayGreeting;

- (void)sayGreeting:(Hello*)greeting;

@end

@implementation Greeting

- (id)init {

if (self = [super init]) {

hello = new Hello();

}

return self;

}

- (void)dealloc {

delete hello;

[super dealloc];}

- (void)sayGreeting {

hello->say_hello();

}

- (void)sayGreeting:(Hello*)greeting {

greeting->say_hello();

}

@end(3)C++类Hello和Objcetive-C类Greeting混合使用,代码如下:

int main() {

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

Greeting *greeting = [[Greeting alloc] init];

[greeting sayGreeting]; //将输出Hello, world!

Hello *hello = new Hello("Bonjour, monde!");

[greeting sayGreeting:hello]; //将输出 Bonjour, monde!

delete hello;

[greeting release];

[pool release];

return 0

}

从上面的代码可以看出,正如可以在Objcetive-C接口中声明C结构一样,也可以在Objcetive-C接口中声明C++类。跟C结构一样,Objcetive-C接口中定义的C++类是全局范围的,不是Objcetive-C类的内嵌类(这与标准C提升嵌套结构定义为文件范围是一致的)。

为了允许基于语言变种条件化编写代码,Objcetive++编译器定义了__cplusplus和__ OBJC__预处理器常量,分别指定C++和Objcetive-C。如前所述,Objcetive++不允许C++类继承自Objcetive-C对象,也不允许Objcetive-C类继承自C++对象。

class Base { /* ... */ };

@interface ObjCClass: Base ... @end // 错误!

class Derived: public ObjCClass ... //错误!

2. 两者的对象模型不能直接兼容

与Objcetive-C不同的是,C++对象是静态类型的,有运行时系统多态是特殊情况。两种语言的对象模型因此不能直接兼容。更根本的原因是,Objcetive-C和C++对象在内存中的布局是互不相容的,也就是说,一般不可能创建一个对象实例从两种语言的角度来看都是有效的。因此,两种类型层次结构不能被混合。

可以在Objcetive-C类内部声明C++类,编译器把这些类当作已声明在全局名称空间来对待,例如:

@interface Foo {

class Bar { ... } // OK

}

@end

Bar *barPtr; // OK

Objcetive-C允许C结构作为实例变量,不管它是否声明在Objcetive-C声明内部。

@interface Foo {

struct CStruct { ... };

struct CStruct bigIvar; // OK

} ... @end

Mac OS X 10.4以后,如果设置fobjc- call-cxx-cdtors编译器标志,就可以使用包含虚函数和有意义的用户自定义零参数构造函数、析构函数的C++类实例来作为实例变量(gcc-4.2默认设置编译器标志fobjc-call-cpp-cdtors)。Objcetive-C成员变量alloc完成以后,alloc函数会按声明顺序调用构造器。构造器使用公共无参数恰当的构造函数。Objcetive-C成员变量dealloc之前,dealloc方法按声明顺序反序调用析构函数。Objcetive-C没有名称空间的概念,不能在C++名称空间内部声明Objcetive-C类,也不能在Objcetive-C类里声明名称空间。

Objcetive-C类、协议、分类不能声明在C++ template里,C++ template也不能声明在Objcetive-C接口、协议、分类的范围内。

但是,Objcetive-C类可以做C++ template的参数,C++ template参数也可以做Objcetive-C消息表达式的接收者或参数(不能通过selector)。

3. 两者有词汇歧义和冲突

Objcetive-C头文件中定义了一些标识符,所有的Objcetive-C程序必须包含这些标识符:id、Class、SEL、IMP和BOOL。

Objcetive-C方法内,编译器预声明了标识符self和super,就像C++中的关键字this。跟C++的this不同的是,self和super是上下文相关的,除Objcetive-C方法外,它们还可以用于普通标识符。

协议内方法的参数列表,有5个上下文相关的关键字(oneway、in、out、inout、bycopy)。这些在其他内容中不是关键字。

从Objcetive-C程序员的角度来看,C++增加了不少新的关键字。你仍然可以使用C++的关键字做OC selector的一部分,所以影响并不严重,但不能使用它们命名Objcetive-C类和实例变量。例如,尽管class是C++的关键字,但是你仍然能够使用 NSObject的class方法。

然而,因为它是一个关键字,所以不能用class做变量名称:

NSObject *class; // Error

Objcetive-C里类名和分类名有单独的命名空间。@interface foo和@interface(foo)能够同时存在在一个源代码中。Objcetive ++中也可以用C++中的类名或结构名来命名你的分类。

协议和template标识符使用语法相同但目的不同:

id foo;

TemplateType bar;

为了避免这种含糊之处,编译器不允许把id做template名称。最后,C++有一个语法歧义,当一个label后面跟了一个表达式表示一个全局名称时,就像下面:

label: ::global_name = 3;

第一个冒号后面需要空格。Objcetive ++有类似情况,也需要一个空格:

receiver selector: ::global_c++_name;

4. 两者功能上有限制

Objcetive C++没有为Objcetive-C类增加C++的功能,也没有为C++类增加Objcetive-C的功能。例如,你不能用Objcetive-C语法调用C++对象,也不能为Objcetive-C对象增加构造函数和析构函数,也不能将this和self互相替换使用。类的体系结构是独立的。C++类不能继承Objcetive-C类,Objcetive-C类也不能继承C++类。另外,多语言异常处理是不支持的。也就是说,一个Objcetive-C抛出的异常不能被C++代码捕获;反过来,C++代

码抛出的异常也不能被Objcetive-C代码捕获。

要点(1)C++和Objcetive-C在定义结构上一样,但是后者的继承是封闭的。(2)Objcetive-C接口中定义的C++类是全局范围的,而不是Objcetive-C类的内嵌类。(3)C++和Objcetive-C的对象模型不能直接兼容。与 Objcetive-C不同的是,C++对象是静态类型的,有运行时系统多态是特殊情况。(4)C++和Objcetive-C有词汇歧义和冲突。(5)C++和Objcetive-C两者功能上有限制。Objcetive C++没有为Objcetive-C类增加C++的功能,也没有为C++类增加Objcetive-C的功能。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载