《iOS开发全然上手——使用iOS 7和Xcode 5开发移动与平板应用》之Objective-C新手训练营
下一步。你将学习Objective-C的基础知识。在此基础之上。将探索类(class)与对象(object)的知识。它们是构建应用程序的主要基石。与此同一时候,你将创建CarValet应用程序,练习一些类的编写。并学习属性(property)的知识。在本章末尾,你将在指导下完毕编程挑战题以探索子类扩展的知识。
当学完本章时,你将获得CarValet应用程序的初始版本号。以及足够完毕本书学习的Objective-C知识。
2.1 使用模板创建Hello World 应用程序
创建Hello World演示样例应用程序的最简单方法,就是使用Xcode的预设模板。在接下来的步骤中,你将创建一个新的项目。然后改动它,使之能打印“Hello World”,并能在iOS模拟器上执行。在创建第一个Xcode项目时,你将学到关于Xcode这一关键工具的整体介绍,以及怎样创建和操纵项目。
2.1.1 创建Hello World 项目
启动Xcode。你将看到如图2-1所看到的的Xcode欢迎页面(假设不小心关闭了这个页面。可通过选择Window | Welcome to Xcode(欢迎使用Xcode)或按下Cmd+Shift+1组合键来又一次显示)。
单击Create a New Project。
也能够在Xcode中选择File | New | Project(或按下Cmd+Shift+N组合键)。之后将会出现如图2-2所看到的的模板选择窗体。
默认情况下,模板选择窗体被嵌套在一个新的、名为工作区(workspace)的大窗体中。这个单独的工作区窗体中包括Xcode所有的编辑器和检查器(inspector)特性。
在图2-2所看到的的模板选择窗体中,选择左边的iOS菜单下的Application,然后选择SingleView Application(单视图应用程序),单击Nextbutton。
警告:截图和功能可能会有所不同
Xcode 会定期更新,这意味着以上操作指令可能会与Xcode 中或苹果公司站点上的实际界面元素有细微不同。当遇到这样的情况时,通常能够找到一些显而易见且能完毕同样指令的办法,比方能够在不同的位置查找同名的控件或组件,此外还能够在类似的区域寻找类似的名称。假设以上方法都行不通。请查看本书配套网址(www.infomit.com/title/9780321862969)上的更新内容。假设还是找不到。请发送反馈信息。
接下来显示的是Xcode项目选项面板(project options panel),如图2-3所看到的。
在这个窗体中,请遵循下列步骤:
(1) 在Product Name中输入HelloWorld。
(2) 将Company Identifier(公司标识符)设置为你所在的公司,使用反向域名命名法。比如,Maurice的公司标识符是com.mauricesharp或com.klmapps,Rod的公司标识符是com.strougo,而Erica的公司标识符是com.sadun。
假设没有公司标识符。苹果公司建议使用edu.self。
(3) 单击Nextbutton。
但假设指定前缀为“MS”。那么Xcode会创建名为MSSlideMenuController的文件和类,从而避免潜在的冲突。
在本书中。为了让命名更简短。并未使用前缀,可是你应当依据自己的开发人员名称或公司名称设置前缀。
专家提示:前缀格式
最典型的类前缀是一小串大写字母(苹果公司推荐使用3 个字母),一般是你或你公司名称的单词首字母。
在公司内部项目或开源项目中,这样的格式非经常见。但可读性并非最好。下面是一个滑动菜单类的各种不同的名称。这并非我实际编写过的一个类。是我想出来的。但最好还是试着观察一下列表中的这些类名条目,依照你浏览代码时的方式,看看哪一个更easy高速识别:
SlideMenuViewController
MSSlideMenuViewController
MS_SlideMenuViewController
msSlideMenuViewController
ms_slideMenuViewController
极其可能的是,所有大写且没有分隔符的版本号最难识别。小写的稍稍easy点。但难以识别的程度仅次于前者。更easy识别的是那些带有分隔符的版本号。你应当选择能与符号名称简易搭配使用的类前缀。
长远来看,你将节省大量的开发与调试时间。
• Product Name (产品名称)——应用程序的名称。它会被显示到设备屏幕上。如你在第15 章“部署应用程序”中将要看到的。兴许还能够对其改动。
• Organization Name (组织名称)——作为自己主动生成的源文件头部凝视的一部分。用作版权属性。
• Devices(设备)——应用程序的目标设备平台。能够是iPhone、iPad 或Universal(通用应用程序)——能够同一时候执行在这两种平台上的应用程序。
上一个Xcode窗体面板询问你Hello World项目的存储位置。
假设愿意的话,Xcode还能够为项目创建本地的git仓库。针对当前这个Hello World演示样例。将Source Control(源代码控制)复选框保持为未选中状态。
选择一个目录并单击创建。如图2-4所看到的。
在单击Createbutton之后,你应该能看到在Xcode中,自己的Hello World新项目已被打开。它带有一些Xcode自己主动生成的文件。至此,你得到一个看似不怎么有趣,可是功能完备的应用程序。假设立马单击Run(执行)button,你的app会显示一个顶部带有状态条的白色屏幕。
开发应用程序时,你将在Xcode中花费大量时间。图2-5显示了展开项目文件并选择ViewController.m文件后的Xcode界面。
图2-5 Xcode 项目界面的组成部分
以下是对Xcode界面的高速导航。
接下来的几章将探讨Xcode界面中不同组件的很多其它细节知识。以下的数字与图2-5中显示的数字一一相应:
(1) 单击Runbutton(在左边),编译应用程序,并在模拟器或设备中启动应用程序。单击并保持(长按)Runbutton。你会看到额外的选项,比方用于測试或分析项目的选项。
右側的button可用于停止正在执行的应用程序、正在执行的构建或已发起的其它动作。
(2) 在这个分为两段的弹出式菜单button中,左段用于选择和编辑方案(Scheme),也就是关于你想执行什么以及怎样执行该应用程序。右段用于选择在哪儿执行应用程序:是在某种模拟器中还是在某个已连接的设备上。
(3) Status(状态)区域向你展示上一动作的执行结果或当前动作的进度。
动作包含构建应用程序、执行应用程序或是下载应用程序到设备上。
(4) Editor(编辑器)button用于配置编辑器(图2-5中标为7的区域)中显示的内容。
左側button在图中已被选中,仅仅显示一件东西。即主编辑区。中间的Assistant(辅助编辑)button将编辑区划分为两块,在右側会显示与主编辑器相关的文件。在右側通常情况下会显示头文件。
最后一个button用于显示源码差异,而且能够查看文件的不同版本号。
这一点在使用源码仓库跟踪代码变动时很实用。
注意,不能同一时候显示辅助编辑和源码视图。
(5) 这个区域控制Xcode中工作区视图的整体布局。第一个button显示或隐藏Navigation(导航器,图2-5中的区域6)。第二个button控制Debugger(调试器。区域10)。
最后一个button显示Utilities(区域8和9)。
(6) Navigator(导航器)能够显示项目的不同视图。顶部的图标控制视图的类型。在图2-5中,被选中的是目录图标,因此这个区域显示的是文件以及基于组(group)的项目导航器。其它图标是:项目符号导航器。用于在全项目范围内进行搜索的放大镜图标,用于查看构建问题的警告三角形图标。还有单元格測试导航器。调试导航器,断点列表。最后是日志视图。easy混淆的一点是,文件和组导航器中显示的文件结构和Finder中的文件结构。这两者能够是不同的。在导航器中,组被显示为目录图标。但这些与Finder中的目录是不同的。假设想让组和Finder目录保持一致。就须要在Finder中加入这些目录,并将源文件保存在正确的位置。
(7) Editor(编辑器)是你将在Xcode中花费大多数时间的地方。眼下,它显示的是源代码编辑器。
它还能够显示Interface Builder(界面生成器)、数据模型编辑器或执行时调试測量仪器。
(8) 工具区域显示了不同种类的工具,包含文件信息、高速帮助(当前被选中的内容)、数据模型细节查看器、视图属性(比方颜色、button标题以及尺寸,包含屏幕大小和约束)。
(9) 工具区域的底部为库区域。
在此处你可以找到Interface Builder中构造应用程序所需的可拖曳组件、代码片段,甚至包含图片和其它媒体文件。注意在工具区域的底部可能仅仅看到含有库选择图标的一行。要展开库。请单击并向上拖动选择器工具栏。
(10) 调试区域包括三个主要部分。
顶部工具栏的左边包括用于在执行中暂停以及单步执行的控件。右側包括当前选中的文件。底部被切割成两个主要区域,一个用于检查变量,另一个用于打开控制台。在整本书中,你将学习到有关的很多其它详细知识,特别是在第14章“Instruments和调试”中。如今你对Xcode已有一定了解,该创建第一个应用程序了。
2.1.3 加入Hello World 标签
默认情况下。Xcode创建设置为执行在iPhone或iPad上的项目;当创建一个项目时。下一个项目使用前一次的设置。新项目一開始就包括你创建自己的应用程序所需的全部类和支持文件。
当执行一个新项目时。它会显示一个空白的白色屏幕。
在屏幕上看到Hello World的最快方法是往项目里加入标签。保持项目打开,运行下面步骤(见图2-6):
(1) 通过观察方案弹出菜单的右側(图2-5中的区域2),确认项目被设置为执行在模拟器中。假设右側没有显示“iPhone Retina(4-inch)”,单击方案弹出菜单的对应一边而且选择这一条。注意下拉菜单包括两部分:左側是方案弹窗,右側同意你选择应用程序在哪里执行。你应仅仅将Hello
World标签加入到iPhone故事板中。于是。应用程序在iPhone而不是iPad上执行是很重要的。
(2) 在Xcode项目导航器中单击并选择Main_iPhone.Storyboard文件。
Interface Builder会在编辑区域打开。在很罕见的情况下,Interface Builder并没有打开。这时你要检查并确定没有单击显示源码差异的第三个button(參见图2-5中的区域4)。假设源码控制button未选中,尝试选择一个.m或.h文件。然后再次选择故事板文件。假设还是无论用,那么退出Xcode并重新启动。
(3) 在右下方的查找面板中。输入label并将这个标签对象拖曳到故事板的画布中。
(4) 在Xcode工作区左側在Utility区域中,在Attributes检查器的文本框内输入Hello World,将这个标签调整得好看一些。
图2-6显示这个标签在视图控制器(view controller)的顶部水平居中。
(5) 单击Runbutton,编译并执行Hello World应用程序。
这就是创建简单Hello World应用程序所需的全部步骤。
你甚至不须要编写不论什么代码。在下一节。你将学习一些Objective-C基础知识,回到这个Hello World应用程序,并加入一些代码以练习Objective-C。
图2-7 所看到的的模拟器是全尺寸的、4 英寸Retina 显示屏,而且可能很大,特别是当在笔记本电脑屏幕上工作时。
能够使用Simulator Window | Scale 菜单更改显示屏的显示大小。
要成为熟练的iOS开发人员,你须要学习Objective-C。它是iOS和Mac的主要编程语言。Objective-C是一种强大的面向对象编程语言,同意你利用苹果公司的Cocoa和Cocoa Touch框架,构建应用程序。在这一章,你将学习主要的Objective-C技巧,開始iOS编程。你将学到接口、方法以及很多其它其它知识。要想完毕这些学习。必须往Hello World应用程序中加入一些自定义的类,练习所学的知识。
这一节涵盖的Objective-C 知识足以让你理解本书所展示的代码,并实现大多数章末尾的挑战题。
然而。Objective-C 有太多的内容。我们没法在这么小的一节里边所有涵盖,而这些没有涵盖的内容中,有一些对于编写产品级质量的应用程序是很重要的。
学习Objective-C的一份重要材料是Learning Objective-C 2.0:A Hands-on Guide to Objective-C for Mac and iOS Developers,2nd edition。作者是Robert Clair。
还能够使用Objective-C Programming:The Big Nerd
Ranch Guide,作者是Aaron Hillegass。这本书涵盖了更高级的知识。
Objective-C是ANSI C的严格超集。C语言20世纪70年代早期由AT&T开发的一种面向过程的编译型语言。Objective-C由Brad J. Cox在20世纪80年代早期开发。向C语言添加了面向对象的特性。
它将C语言的组成部分与Smalltalk-80中产生的概念加以混合。
Smalltalk是最老和最有名的面向对象编程语言之中的一个。是由Xerox PARC开发的一种动态类型的交互式语言。Cox将Smalltalk的对象和消息传递系统层叠到标准C语言之上,从而构建了一种新的语言。这样的方法同意应用程序猿在继续使用熟悉的C语言进行开发的同一时候。在那种语言内部使用基于对象的特性。在20世纪80年代后期,Objective-C被史蒂夫·乔布斯的计算机创业公司Next的NeXTStep操作系统选作主要开发语言。NeXTStep成为OS
X直到iOS的精神和字面意义上的先驱。
Objective-C 2.0在2007年10月随着OS X Leopard一起公布,它引入了很多新特性,如属性和高速迭代。
在2010年,苹果公司更新了Objective-C语言,添加了Block这个C语言扩展。它提供了匿名函数。并同意开发人员将Block作为对象进行处理(你将在第13章中学习很多其它内容)。在2011年夏,苹果公司引入了自己主动引用计数(Automatic
Reference Counting,ARC),这个扩展大大简化了开发,它同意应用程序猿将注意力集中在应用程序语义上,而不用操心内存管理(准确地说,ARC是编译时扩展而非语言扩展)。近期,Objective-C被扩展为支持字面量(定义静态对象的方式)以及索引(訪问数组和字典中元素的方式)。苹果公司持续改进Objective-C语
言。所以要注意新的iOS和Xcode更新版本号的公布说明。
面向对象编程使用了ANSI C没有的表特性。
对象是一种数据结构。它关联了一组公开声明的函数调用。Objective-C中的每一个对象包括一些实例变量(instance variable)。也就是这样的数据结构的数据域(或字段)。还包括一些方法(method)。也就是该对象所能运行的函数调用。面向对象的代码使用这些对象、变量和方法来引入一些编程抽象来添加代码的可读性和可靠性。你有时候可能会看到实例变量被缩写为iVar,方法被称作消息(message)。
对象使用类(class)进行定义。
能够将类觉得是决定对象终于长什么样的模板:怎样查看状态(实例变量),以及支持什么行为(消息)。
类本身通常不做太多事情。它们的主要用途是创建功能完整的对象。
对象被称作实例(instance),也就是基于类所提供模板的起作用的实体。之所以命名为“实例变量”。是由于它们仅仅存在于类的实例中。而不是类自身的内部。当往第4步的文本框中输入“Hello World”时。实际上也就设置了一个UILabel对象的text实例变量的值。
UILabel类本身并没有text变量可被设置。
全部这些事情,以及创建标签实例的代码已经自己主动完毕了。
面向对象编程让你构建可重用的代码并从面向过程开发的正常控制流中解耦。
面向对象的应用程序环绕着对象和它们的方法所提供的自己定义数据结构来开发。iOS的Cocoa Touch以及Mac OS X的Cocoa提供了一个包括大量这样的自己定义对象的仓库。
Objective-C解锁了那个仓库,并同意你用最少的努力和代码。基于苹果公司的工具箱创建有效而强大的应用程序。
注意:
iOS 的Cocoa Touch 中以NS 开头的类名。比如NSString 和NSArray,可追溯到NeXT公司。NS 代表NeXTStep,执行在NeXT 计算机之上的操作系统。苹果公司于1996 年收购了NeXT。
Objective-C是C语言的超集,正由于如此,它沿用了大多数同样的语法。
让大多数Objective-C的学习者感到困惑的一点,在于消息传递的语法——用于调用或执行类实例所实现的方法。不像函数调用时使用的“函数名(參数列表)”语法。传递消息给对象时要使用方括号。
一条消息让一个对象运行一个方法。
实现这种方法。产生一个结果,是这个对象的职责。方括号里的第一个元素是消息的接收者,也就是实现这种方法的对象;第二个元素是方法名称以及可能会有的传给那个方法的一些參数。它们一起定义了你想要发送的消息。在C语言中。你可能会这么写:
<span style="font-size:14px;">printCarInfo(); // This function prints out the info on the default car</span>
可是在Objective-C里,你这么写:
<span style="font-size:14px;">[self printCarInfo]; // This method prints out the info on the default car</span>
在C语言中,想要执行这个函数的目标对象被假定为当前的对象。
在某些语言中,你可能看到this.printCarInfo()。
在Objective-C中,self代表当前对象,大体上与this类似。
在其它语言中,你可能会使用类似someOtherObject.printCarInfo()的代码。在还有一个对象上调用方法,如果someOtherObject拥printCarInfo()函数。在Objective-C中,能够使用下面代码:
<span style="font-size:14px;">[someOtherObject printCarInfo]; // This method prints out the info on the default car</span>
虽然语法不同,但方法(method)基本上讲就是在对象上运行的函数(function)。
除了Objective-C的类型之外,方法中的类型能够使用标准C语言中相同的类型。
不像函数调用。Objective-C限制了能够实现和调用方法的主体。方法属于类。而且类的接口定义了哪些方法是公开的。或者说。是面向外部世界的声明。
当函数包括一个或多个參数时,代码就開始看起来不一样了。假定必须将汽车对象myCar 传递给printCarInfo()函数。
在C语言中,你会这么写:
printCarInfo(myCar); // Print the info from the myCar object
在Objective-C中,你会这么写:
<span style="font-size:14px;">[self printCarInfo:myCar]; // Objective-C equivalent, but with poor method name</span>
在Objective-C中。你被鼓舞依次放置方法名和參数。因此极其可能将printCarInfo方法重命名为:
<span style="font-size:14px;">[self printCarInfoWithCar:myCar]; // More clear as to which car it will print out</span>
如今将演示样例再深入推进一步。假定必须传递显示信息时的字体大小。
在C语言中,你会这么写:
<span style="font-size:14px;">printCarInfo(myCar,10); // Print the info using a font size of 10</span>
在Objective-C中。你会使用例如以下代码:
<span style="font-size:14px;">[self printCarInfoWithCar:myCar withFontSize:10]; // Print using a font size of 10</span>
通读代码时能够立马发现Objective-C清晰了不少。
让我们再深入一步。如今假定你有三个參数:汽车对象,信息的字号。还有表示文字是否须要粗体显示的布尔值。
在C语言中,你会使用例如以下代码:
<span style="font-size:14px;">printCarInfo(myCar, 10, 1); // Using 1 to represent the value of true in C</span>
在Objective-C中,你会这么写:
<span style="font-size:14px;">[self printCarInfoWithCar:myCar withFontSize:10 shouldBoldText:YES];</span>
注意:
在Objective/Cocoa 中,布尔值由BOOL 类型提供。BOOL 类型的标准值是YES/NO而不像C 语言中的true/false。虽然也能够引入C 标准库,而且使用C 语言的Boolean 类型,但不推荐。
方法名与參数依次交错放置,能有效地让Objective-C 的消息传递变得easy阅读和理解。在C 语言和其它语言中,你不得不时常參考函数定义以确定每一个參数是什么以及參数的顺序。在Objective-C 中。这些都非常清晰。就在你面前。在使用一些含有5 个或很多其它个參数的UIKit 方法调用时,你会特别明显地体会到这一点。
在C语言中。你会使用下面代码:
<span style="font-size:14px;">float mySpeed = calculateSpeed(100,10); // returns the speed based on distance / time</span>
在Objective-C中。你的方法调用看起来例如以下所看到的:
<span style="font-size:14px;">float mySpeed = [self calculateSpeedWithDistance:100 time:10];</span>
注意:
假设Objective-C 的方法声明和消息传递语法对你来说还是不清楚,不要操心。在本章的下一节。你将有充分机会加以练习。
苹果公司提供图2-8 所看到的的文字来阐述Objective-C 方法调用的组成部分。
要看到很多其它信息,
请看https://developer.apple.com/library/iOS/#referencelibrary/GettingStarted/RoadMapiOS/Languages/
WritingObjective-CCode/WriteObjective-CCode/WriteObjective-CCode/html。
方法能够訪问类中定义的全部东西。换句话说,能够訪问实例变量以及随意类实例中实现的方法。
在这样的意义上。方法如何执行对于调用者对象是透明的。
某个特定方法的实现代码甚至是整个类能够全然改变而不须要将别的不论什么地方改动。
这在升级或替换应用程序中的特性时是很实用的:可能是让它们更有效地更新新的硬件特性,甚至彻底替换通信的处理方式。
2.2.2 类和对象
对象是面向对象编程的核心。
能够通过构建类定义对象。类即为创建对象的模板。
在Objective-C中。类定义描写叙述了怎样构建属于这个类的新对象。比如,要创建汽车对象。须要定义Car类。而且在须要时用这个类创建新的对象。
与C语言类似。在Objective-C中实现类需要分两处进行:头文件和实现文件。头文件规定了外部世界怎样与这个类交互:实例变量及其类型。方法及其參数和返回值类型。就像契约,头文件承诺类的实例怎样与别的对象对接。
实现文件的内容即为类怎样提供实例变量的值,以及方法被调用时怎样响应。除了头文件里定义的公有变量和方法。在实现文件里也能够定义变量与方法,而且实现文件一般会包括私有的变量和方法。
每一个类使用标准的C.h约定。在头文件里列出实例变量和方法。比如。你可能会像代码清单2-1那样定义SimpleCar对象。此处所看到的的Car.h头文件包括声明SimpleCar对象结构的接口。
代码清单2-1 声明SimpleCar 接口(SimpleCar.h)
<span style="font-size:14px;">#import <Foundation/Foundation.h> @interface SimpleCar : NSObject { NSString *_make; NSString *_model; int _year; } @property float fuelAmount; - (void)configureCarWithMake:(NSString*)make model:(NSString*)model year:(int)year; - (void)printCarInfo; - (int)year; - (NSString*)make; - (NSString*)model; @end</span>
在Objective-C中,类、变量和方法採用驼峰式命名法。
在Objective-C中。你会使用identifiersLikeThis而不是identifiers_like_this。
类名首字母大写,而其它的名称首字母小写。
能够在代码清单2-1中看到。类名SimpleCar以大写首字母开头。实例变量fuelAmount採用驼峰式命名法。可是以小写字母开头。
示比例如以下:
- (void) printCarInfo
在Objective-C中。@符号用在特定的一些keyword中。此处展示的两个元素(@interface和@end)划分了类接口定义的开头与结尾。类定义描写叙述了一个含有5个方法与4个实例变量的对象。
在这4个变量中,仅仅有fuelAmount是公有的(public),意思是它在SimpleCar类的外部能够使用。
花括号里的其它三个变量仅仅能被SimpleCar及其子类使用。这三个变量也能够定义在.m实现文件里,但那样的话它们将仅仅对SimpleCar类可见。假设想让子类(比方ElectricCar)共享这些变量的话,这就成问题了。
私有变量中有两个(_make和_model)是字符串类型。Objective-C通常使用基于NSString对象的类,而不是基于字节(byte)的用char *声明类型的C字符串。正如在这本书中到处可见的。NSString提供的功能远远多于C字符串。对于这个类。能够找出字符串的长度。查找和替换子字符串,颠倒字符串。提取文件扩展名。以及很多其它。这些特性都被编写到iOS(和Mac OS)的对象库里。
私有的_year变量和公有的fuelAmount变量都属于简单类型。前者是int类型,后者是float类型。
使用前置的下划线字符(_)是Objective-C中区分实例变量和getter方法的一般做法。使用x=_year是直接从实例变量获得值,而x=[self year]是调用getter方法-(int)year。能够做一些必要的计算或者在返回之前暂时计算值。setter方法类似getter,仅仅只是是用于设置实例变量的值。
反复一下,使用setter能够运行一些额外的工作,比方更新屏幕上的计数器。
你将在下面内容中以及全书中学习创建与使用getter和setter。
第一个公有方法例如以下所看到的:
<span style="font-size:14px;">configureCarWithMake:model:year:</span>
这个完整的包括三段的声明(包括冒号)。才是该方法的名称或选择器(selector,有时也称为签名)。那是由于Objective-C在方法名中交替放置參数,使用冒号分隔每一个參数。在C语言中,能够使用setProperties(char *c1,char *c2,int i)这种函数。Objective-C的方式,虽然更繁重,但提供更好的清晰性和不言自明的文档。你不须要推測c1和c2的作用,由于它们的用途直接在方法名称里声明了:
<span style="font-size:14px;">[myCar configureWithMake:c1 model:c2 year:i];</span>
每一个方法都有返回參数。printCarInfo返回void,year返回int。而make和model都返回NSString*类型。与C一样。这些代表方法返回的数据类型。
void代表这种方法不返回不论什么东西。
在C语言中。与printCarInfo和year方法等价的函数声明是void printCarInfo()和int year()。
使用Objective-C的“方法名分散在參数中”的方式。对新应用程序猿来说可能看起来非常奇怪,可是非常快就会变成你非常喜爱的特性。
当方法名告诉你什么什么參数该在哪儿时,就不需要推測该传递什么參数。
你会在iOS编程中多次见到这个。特别是当使用respondsToSelector:这种方法调用时,这种方法让你在执行时检查对象能否响应特定的消息。
注意代码清单2-1中的头文件使用#import载入头文件,而不是#include。当导入(import)头文件时,Objective-C自己主动跳过已经被加入了的文件。
因此能够往各种各样的头文件里加入@import指令,而不会有不论什么损失。
1. 定义实现文件
.h文件告诉外界怎样与类的对象交互。.m文件或实现文件包括了赋予对象生命与力量的代码。代码清单2-2展示了SimpleCar类的某种实现。
代码清单2-2 SimpleCar 类的实现文件(SimpleCar.m)
<span style="font-size:14px;">#import "SimpleCar.h" @implementation SimpleCar - (void)configureCarWithMake:(NSString*)make model:(NSString*)model year:(int)year { _make = [make copy]; _model = [model copy]; _year = year; } - (void)printCarInfo { NSLog(@"--SimpleCar-- Make: %@ - Model: %@ - Year: %d - Fuel: %0.2f", _make, _model, _year, [self fuelAmount]); } - (int)year { return _year; } - (NSString*)make { return [_make copy]; } - (NSString*)model { return [_model copy]; } @end</span>
实现文件通常与头文件配对,因此第一件事情即为导入那个头文件。在此处是SimpleCar.h。大多数类会导入其它头文件,而且可能声明常量或做其它事情。类实现的主要部分位于@implementation和@end之间。
configureCarWithMake:model:year为每一个私有实例变量设置值。
除了fuelAmount之外,不能为当前的汽车对象单独设置某个值。
使用訪问器方法(access method)读取不论什么单个元素的值是可行的。比如代码清单2-2底部定义的-(int)year。
由于头文件为fuelAmount使用了@property,所以setter和getter方法,以及下划线版本号的变量已经为你创建好了。
你将在本章后边的“2.3.2节“属性”中看到相关很多其它内容。
第一个方法设置三个非公有实例变量的值。printCarInfo将全部实例变量的当前值打印到日志中。最后三个方法是私有实例变量的getter方法。
你可能注意到的一点是。配置方法和getter方法都与字符串的副本打交道。这是一种普遍的防御性实践。避免代码意外地改动字符串的值。但当你意识到每一个变量是一个指向NSString对象的指针时,就会理解了。假设将这个指针赋值给字符串參数。那么它将与这个字符串的拥有者指向同样的内存位置。假设原始拥有者改变了字符串的值。汽车对象会得到新的值,由于它指向同样的内存地址。
赋值并返回字符串的副本,会得到不同内存区域的新对象。
这些副本能够被改动。而不会改变当前汽车对象的make和model。注意。你唯一须要注意的是,这仅仅适用于长期存在的实例变量。暂时字符串和对象不须要被拷贝。
2. 创建对象
你已经学到,类定义一个或很多其它个对象。
类在执行时怎样变成对象?要创建对象。你需要让类为新对象分配足够的内存,而且返回一个指向这块内存的指针。
然后让新对象初始化自己。你通过调用alloc方法处理内存分配,而且初始化发生在调用init时。假设正在创建SimpleCar对象。那么能够使用例如以下两行代码:
<span style="font-size:14px;">SimpleCar *myCar = [SimpleCar alloc]; [myCar init];</span>
虽然看起来没有多少代码,但这将是你时常须要输入的。幸运的是,Objective-C支持嵌套消息发送。这意味能够使用一个方法的结果作为还有一个方法的接收者。
一组嵌套消息的返回值来自最后一条消息。
在之前的代码中,第一行为汽车对象分配内存而且返回一个指向那个对象的指针。第二行拿到了分配好的汽车对象而且加以初始化。init方法返回初始化之后的对象。
由于myCar已经指向正确的对象,而第二行不须要使用这个返回值。使用嵌套能够将这两行缩短为一行:
<span style="font-size:14px;">SimpleCar *myCar = [[SimpleCar alloc] init];</span>
此处。将消息alloc发送到SimpleCar对象的源码得到一个新的汽车对象,然后将消息init发送到新分配好的SimpleCar对象并返回初始化后的汽车对象。这样的嵌套在Objective-C中是非常典型的。
你在此处看到的“分配后紧跟init”的模式是实例化对象的最常见方式。SimpleCar类指向alloc方法。
它分配足够存储类定义中全部实例变量的新内存块,将全部实例变量清理为0或nil,并返回指向这个内存块開始位置的指针。
新分配的块是实例,代表内存中单独的对象。
某些类,比方视图。使用自己定义的初始化方法,比如initWithFrame:。如你在本章后边将看到的,能够编写自己定义的初始化方法。比方initWithMake:model:year:fuelAmount:。这种紧随内存分配进行初始化的模式广泛存在。
你在内存中创建这个对象。然后预设全部关键的实例变量。
3. 继承方法
对象在继承实例变量的同一时候会继承方法实现。SimpleCar是一种NSObject,因此全部NSObject可以响应的消息Simple Car也可以响应。这就是myCar可以使用alloc和init进行初始化的原因。
这两个方法是由NSObject定义的,可用于创建和初始化不论什么SimpleCar实例。由于它继承自NSObject类。Objective-C中的全部类都终于继承自NSObject,NSObject处于它们继承树的顶端。
提示:继承的方法
假设查看Hello World 项目的AppDelegate 或ViewController 类的.h 文件。就会看到AppDelegate 继承自UIResponder,而且ViewController 继承自UIViewController。而UIViewController 接下来继承自UIResponder。
假设选择并右击UIResponder,选择Jump to Definition(跳到定义),那么Xcode 会为你显示UIResponder 的声明。在那里能够看到,它也继承自NSObject。
作为还有一个演示样例,当应用程序中有数组时,你有可能会使用NSArray或NSMutableArray—— 一种同意你增删元素的NSArray。全部数组方法都能够被可改动的数组以及它们的子类使用。能够统计数组中元素的个数。依据索引数字取出对象等。
警告:
有些类对“子类化”(subclassing)并不友好。
它们是作为类簇(class cluster)来实现的。也就是,类自身会依据一些标准,创建一些其它类的对象。NSArray 和NSString 都是类簇的演示样例。它们以最能有效利用内存的方式。使用不同的类创建对象。全部这些类都在文档中做了清晰标记。在子类化系统类之前,要细致检查一下其是否为类簇。
子类能够实现与超类具有同样选择器的方法。
在子类对象上调用此方法将运行新的方法。这取决于这种方法怎样实现,要么特殊化。要么覆盖超类行为。
特殊化(Specializing)的意思是(运行新逻辑的同一时候)还让超类方法运行,方法是将消息发送到super对象,super是代表超类的特殊标识符。覆盖(Overriding)的意思是并不将消息发送到超类,超类的行为从不运行。一个不错的演示样例就是初始化方法。须要确保继承链中的每个类都有计划初始化自身。
只是方法仅仅须要记得调用自身的超类。
初始化方法总是包括下面形式的一行:
<span style="font-size:14px;">self = [super init];</span>
这会将当前对象的实例设置为超类创建的实例。接下来就是初始化对象的剩余部分。
警告:先初始化超类
很重要的是,要在做不论什么特定于类的事情之前调用超类初始化。假设试着首先操作对象。此时不论什么超类,包含NSObject,所提供的一切东西都没有建立起来。虽然实例变量能够返回一个值。而且方法调用可能顺利进行。可是这个对象将处于一种没有定义的状态。
最好的情况是。在初始化时遇到一次应用程序崩溃(crash),而更有可能的是,在之后某一天将遇到一些随机性的崩溃或者奇怪的行为。你将在后边学到怎样编写正确的init 方法。
4. 指向对象
你已经了解到,使用类创建对象是很easy的事情。当拥有一个新创建(分配)和初始化的对象时。下一步就是引用并使用这个新对象。在Objective-C中,你使用*字符表示变量是指向对象的指针,这在代码清单2-2中的_make和_model变量声明处能够看到。_make和_model变量都指向对象(NSString)而且变量名前边必须有*。
其它变量属于原始类型,不是对象。
变量自身,或更准确说内存地址,保存的是值而不是对象的地址。_year变量是原始类型(int)的演示样例,因此不须要*字符。
当向对象发送消息时。要去掉*字符。在下面代码片段中。myCar对象被创建为SimpleCar类的实例,而且printCarInfo方法被调用:
<span style="font-size:14px;">SimpleCar *myCar = [[SimpleCar alloc] init]; [myCar printCarInfo];</span>
假设想创建指向同一个SimpleCar对象的还有一个指针,能够使用下面代码:
<span style="font-size:14px;">SimpleCar *sameCar = myCar;</span>
或这段代码:
<span style="font-size:14px;">id sameCar = myCar;</span>
id是一种会被翻译为NSObject *的特殊类型,这个翻译结果已经包括*字符。表示指向对象的指针。
意识到myCar和sameCar指向同样对象是很重要的。假设使用sameCar指针改动make、model和year,那么[myCar printCarInfo]会显示新的值。
2.3 CarValet 应用程序:实现Car 类
如今是时候练习使用Objective-C语言创建方法、变量和类了。一開始先为CarValet应用程序创建项目。当继续阅读这本书时,你会将这个应用程序从简单逐步開始变成完整的iOS应用程序,它能够执行在iPhone和iPad上,使用原生用户界面元素和iOS的其它部分。
第一步是创建CarValet应用程序项目。
使用与创建HelloWorld项目时同样的步骤:
(1) 在Xcode中,选择File | New | Project(或按下Cmd+Shift+N组合键)。
(2) 选择iOS Single View Application模板,与你创建HelloWorld项目时选中的模板同样。
(3) 在下一个面板中,在应用程序的名称文本框中输入CarValet。
确保Devices被设置为Universal。Organization和Company Identifier文本框中应该已经填入你在创建HelloWorld项目时填写的内容。
假设须要的话能够改动这些内容,然后单击Nextbutton。
(4) 保存项目,Xcode就会在新窗体中打开这个项目。假设已经打开一个项目。那么在Save面板的底部可能会有Add To(加入到)选项。假设是这种话。确保选中的是类似“Don’t Add to Any Project or Workspace”(不要加入到不论什么项目或工作区)的选项。
注意:
这个演示样例应用程序中的代码——以及本章中其它演示样例的代码——都能够在本书的演示样例代码中找到。參见前言。了解从GitHub 下载本书演示样例代码的具体方法。
与HelloWrold 全然一样。 CarValet 应用程序已经带有Xcode 模板提供的两个类:
AppDelegate和ViewController。如今添加Car类以表示简单的汽车:
(1) 首先,右击Navigation窗体中的CarValet应用程序目录,并选择New File。也能够在菜单中选择File | New | File或按下Cmd+N快捷键。
(2) 在新文件对话框中,选择iOS下的Cocoa Touch。然后选择Objective-C class。如图2-9所看到的,然后单击Nextbutton。
(4) 保存面板的底部有一块区域可用于指定Target成员。此时,重要的是确保CarValet被选中。如图2-11所看到的。在确认该复选框已被选中后,单击Create,Xcode会创建Car类。而且将它放到你的项目中。
编辑Car.h头文件,使它与代码清单2-3保持一致。
代码清单2-3 Car.h 头文件 // Car.h // CarValet #import <Foundation/Foundation.h> // 1 @interface Car : NSObject { // 2 int _year; // 3 NSString *_make; // 4 NSString *_model; // 5 float _fuelAmount; // 6 } - (id)initWithMake:(NSString *)make // 7 model:(NSString *)model year:(int)year fuelAmount:(float)fuelAmount; - (void)printCarInfo; // 8 - (float)fuelAmount; // 9 - (void)setFuelAmount:(float)fuelAmount; - (int)year; // 10 - (NSString*)make; - (NSString*)model; @end
前两行是凝视。在Objective-C中,编译器会忽略双斜杠(//)后边的文字。
双斜杠可用于单行或行内凝视。能够使用斜杠和星号的组合来包围凝视块——也就是多行凝视:
// this is a one line comment // and so is this, even though it follows the last one [MyObject doSomething]; // and this is an end of line comment /* And finally a lot of comments started by a forward-slash and asterisk that can include lots of lines and ends with an asterisk then forward-slash. */
第一个非凝视行导入了Foundation框架。这个iOS和Mac OS家中的耕田老牛。在Foundation框架中。能够找到各种东西,从数组和日期到谓词,从URL网络连接到JSON处理,还有最最重要的对象NSObject。
接下来是Car类的@interface声明。
通过:NSObject标记,能够了解到Car类继承自NSObject类。
@interface和@end之间的语句对Car类进行了定义。在@interface声明的花括号里,能够看到4个实例变量。用于保存汽车对象所须要的信息。这些方法定义了怎样给汽车对象发送消息。
以下描写叙述了代码清单2-3中带数字凝视的代码行中所发生的事情:(1) 导入Foundation框架。
(2) 定义Car对象(NSObject的子类)的接口。
(3) _year是汽车的生产年份,存为一个总体对象,这是一种非对象的原始类型。
(4) _make是汽车的品牌,存为一个NSString对象。
(5) _model是汽车的型号。存为还有一个NSString对象。
(6) _fuel是汽车油箱里的燃料,存为浮点值。
(7) initWithMake:model:year:fuelAmount:方法初始化新分配的对象。并设置汽车的品牌、型号和年份,以及油箱中的燃料。
这就是前面所说的自己定义init方法。
(8) printCarInfo方法向调试控制台打印出汽车的信息。
(9) fuelAmount和setFuelAmount这一对方法,为_fuelAmount实例变量的getter和setter方法。
(10) 剩下的三个方法是其它私有实例变量的getter方法。
initWithMake:model:year:fuelAmount:方法是关于方法名称和參数怎样交替放置的清晰示例。这4个參数分别接收NSString *、NSString *、int和float类型的值。
注意这两个方法前面的连字符,它表明这些方法由对象实例进行实现。比如,要调用[myCar printCarInfo]而不是[CarprintCarInfo]。
后者会将消息发送到Car类而不是实际的Car对象。你会在本书后面看到类方法和实例方法的对照差别(类方法由“+”而不是“-”表示),只是,更加完整的讨论超出了本书范围。
方法调用能够非常长。比如,以下的方法调用初始化iOS的UIAlert对象:
initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:
每一个參数表示在警告框中显示或可选显示的元素。包含标题、消息、Cancelbutton文字以及其它button的文字。还有一个參数表示delegate(托付对象)。
使用托付对象是iOS中的还有一常见模式,它提供了一种方法。通过让两个对象使用定义好的一组消息(称为protocal(协议)来进行通信。这些消息是协议提供者和托付对象之间的一种契约。本质上,托付对象承诺实现这些消息,而提供者承诺正确地使用它们。协议与类是分开定义的;它们是不同的事物。不论什么类能够选择以托付对象或实现者的身份使用协议。UIAlert採用这样的模式以通知托付对象。用户单击了哪个button。当阅读这本书时,你会看到系统对象中更加复杂地使用托付模式的演示样例。你还会在自己构建的一些对象里实现这样的模式——也就是创建协议并向对象加入托付对象代码。
在Xcode 中,组合键Ctrl+Cmd+向上箭头能够让你移到下一个配对文件。而Ctrl+Cmd+向下箭头能够让你移到前一个配对文件。
这对组合键使得在头文件和实现文件之间切换变得很easy。
头文件和实现文件共同存储了一个类的实现以及在应用程序其它部分使用该类所需的所有信息。
实现的源码通常包括在.m文件里(m能够代表implementation或method)。顾名思义,类的方法文件提供的是方法的实现。以及这个类怎样运行它的功能。
在更大的类中。除了会在.h文件里定义方法外,你可能会实现其它的得到支持的非公有方法。
不像公有方法。不须要在定义私有方法前声明它们。编译器足够聪明,甚至在使用这些方法的代码的后边才实现这些方法的情况下。编译器也能够识别私有方法在哪里。
在通读本书的过程中。你会看到关于这一点的很多其它内容。
此外。能够声明仅对这个对象可见的局部实例变量。能够在@implement语句以下的花括号中实现这一点。
比如。能够加入局部的isAL emon标记变量:
@implementation Car { BOOL isALemon; }
对于大多数含有实例变量的对象,典型地,实现的第一个方法会是init。以下通过复制代码清单2-4中的粗体代码到Car.m实现文件里,能够创建这种方法的第一个版本号。
代码清单2-4 Car.m 实现文件 // Car.m // CarVale #import "Car.h" @implementation Car - (id)init { self = [super init]; // 1 if(self != nil) { // 2 _year = 1900; // 3 _fuelAmount = 0.0f; // 4 } return self; // 5 }
以下描写叙述了在代码清单2-4中。带数字凝视的代码行中所发生的事情:
(1) 第一个任务是在超类(NSObject)上调用init方法。
这保证了NSObject要求的不论什么初始化逻辑。在特定于Car类的初始化逻辑之前运行完成。
(2) 检查一下。确保self实际已经初始化。假设这种话,这个对象的剩余部分将被创建。
(3) _year实例变量默认被设置为1900。
(4) _fuelAmount默认被设置为0.0f。虽然不是严格必要的,但在数字末尾包括f能够告诉编译器这是float值。而不是其它类型的浮点值。
(5) self的值被返回。注意返回的内容依赖于第2步的检查。假设超类返回nil,此处返回nil,否则会返回现已初始化的Car对象。
到此为止,Car对象已经被初始化,但仍然不能响应以initWithMake:开头的自己定义初始化方法和printCarInfo方法,或者不论什么其它方法调用。假设试图调用那些方法,你将遇到执行时应用程序崩溃,同一时候会在Debug区域的Console框中显示一条消息“unrecognized selector sentto instance”。
向nil 发送一条消息,并发送一条未识别的消息(选择器)
在Objective-C 中。发送消息和执行选择器这两个术语在本质上是同一个东西:在对象上调用方法。
虽然向nil 发送不论什么消息都是全然安全的。但向一个未实现这条消息的对象发送一条消息会导致执行时应用程序崩溃。这是Objective-C 刚開始学习的人常犯的错误。而且正如你将在本书后边看到的,存在一些方法,可以在发送消息之前检查对象或类能否响应这条消息(或这个选择器)。此外还要
注意,消息能够在继承树的不论什么地方实现。
也就是说,这条消息能够在特定的类中定义。也能够在这个对象继承的不论什么类中定义。
通过加入代码清单2-5的内容。添加下列两个方法:自己定义初始化方法以及printCarInfo方法。代码清单2-5 Car.m 实现initWithMake:model:year:fuelAmount:和printCarInfo 方法 - (id)initWithMake:(NSString *)make // 1 model:(NSString *)model year:(int)year fuelAmount:(float)fuelAmount { self = [super init]; // 2 if(self != nil) { // 3 _make = [make copy]; // 4 _model = [model copy]; _year = year; _fuelAmount = fuelAmount; } return self; // 5 } - (void)printCarInfo { if(!_make) return; // 6 if(!_model) return; NSLog(@"Car Make: %@", _make); // 7 NSLog(@"Car Model: %@", _model); NSLog(@"Car Year: %d", _year); NSLog(@"Number of Gallons in Tank: %0.2f", _fuelAmount); }
以下描写叙述了在代码清单2-5中,带数字凝视的代码行中所发生的事情:
(1) initWithMake:model:year:fuelAmount:分配新的对象。然后将每一个值传入Car对象的属性中。
(2) 首先调用超类的初始化方法。
(3) 检查超类是否可以初始化这个对象。而且假设成功的话,初始化对象的剩余部分。
假设失败的话,self的值会是nil。
(4) 如今为Car对象设置全部实例变量。
(5) 到此为止,self要么是nil(假设超类初始化失败的话),要么是已经初始化的对象。
注意当初始化失败时,返回nil是正确的做法。
(6) 仅当Car定义了make和model时才会信息打印。
(7) 用NSLog在控制台打印值。
真实的左花括号使用惯例本书的代码清单让方法的左花括号({)紧随方法名。
这是为了节省空间,而不是典型的代码惯例。
依照惯例。花括号单独占一行。能够在Xcode 自己主动生成的代码中看到,如
ViewController.m。
1. 基本初始化方法
你在代码清单2-4中做的全部事情都能够由代码清单2-5中的自己定义初始化方法来实现。
为避免反复劳动,能够将第一个方法简化为例如以下代码:
- (id)init { return [self initWithMake:nil model:nil year:1900 fuelAmount:0.0f]; }
initWithMake:model:year:fuelAmount:同意调用者指定全部公有实例变量的值——也就是说,完整地指定新的汽车对象的初始状态。
不论什么其它的初始化方法都能够调用这个完整的初始化方法。这是Objective-C的还有一常见模式。
initWithMake:model:year:fuelAmount:被称作基本初始化方法(base
initializer),由于它是不论什么其它自己定义初始化方法都能够调用的最主要的一个。你将在整本书中看到这个模式的使用。
2. 訪问器
.h文件里声明的最后5个方法用于訪问汽车对象的信息。在这个演示样例中。也就是实例变量。在Car.m文件的底部加入代码清单2-6中的代码。
代码清单2-6 Car.m 文件里。訪问器方法的实现 - (float)fuelAmount { return _fuelAmount; // 1 } - (void)setFuelAmount:(float)fuelAmount{ _fuelAmount = fuelAmount; // 2 } - (int)year { // 3 return _year; } - (NSString*)make { return [_make copy]; } - (NSString*)model { return [_model copy]; }
(1) 返回_fuelAmount实例变量的当前值。
(2) 将_fuelAmount实例变量的值设置为fuelAmount參数的值。
(3) 定义剩下的实例变量的getter方法。
每一个getter方法都会返回相关的实例变量的值。通常。每一个公有的实例变量都能够用getter和setter隐藏起来。变量自己能够在.m文件里声明,这样就仅仅有它们的汽车对象能够直接訪问这些变量。即使在这个简单的类里。这也意味着须要声明和定义8个额外的方法,以及大量的反复代码。
幸运的是。有一种更好的方法。
2.3.2 属性
属性让你定义实例变量,并让编译器创建訪问器方法——也就是说,能够訪问(get或set)变量或信息的方法。
编译器还能够生成下划线版本号的变量。声明属性是非常easy的:
@property float fuelAmount;
这会让编译器创建一个实例变量和两个方法:
float _fuelAmount; - (float)fuelAmount; - (void)setFuelAmount:(float)fuelAmount;
你可能会注意到。此处的变量和方法的定义与代码清单2-3中的对应内容同样。
编译器会为你生成下划线版本号的变量。
不论什么非汽车对象都必须使用getter和setter方法。
变量和方法的实现是在编译时被加入的。而假设须要做一些特殊的事情,那么能够在.m文件里,实现特定的訪问器方法。这样,编译器就会使用你的方法替代。
遵循下面步骤更新Car对象以使用属性:
(1) 在编辑器中打开Car.m文件,并移除fuelAmount、setFuelAmount、year、make和model这些方法的实现。
(2) 打开Car.h并移除你在第1步中删除的那些方法的声明。
(3) 改动头文件里定义这些实例变量的部分,与下面内容一样(新代码以粗体显示,确保删掉下划线):
@interface Car : NSObject
@property int year;
@property NSString *make;
@property NSString *model;
@property float fuelAmount;
- (id)initWithMake:(NSString *)make
使用属性可能看起来是多余的。
毕竟代码清单2-3中的类定义代码定义了訪问器,而且下划线实例变量是私有的。
那么为什么使用属性?原来,除了节省空间之外,使用属性比使用公开声明的方法还有很多其它优点。特别是封装和点表示法。
1. 封装
封装同意在应用程序中的其它部分。包含不论什么使用对象的client,隐藏实现细节。对象的内部表示(实例变量)和行为(方法等)与对象向外界声明自身的方式隔离。仅仅要公开声明的细节保持不变,就能够自由地彻底改动内部实现。属性提供的是,以一种结构良好而且受限地暴露对象状态和其它信息的方式。
然而,属性并不限于用作公有变量。它们在类的定义中也可发挥重要作用。属性同意向类定义的其它部分加入时尚的前瞻性开发技术,包含延迟初始化(lazy loading)和缓存。
这就是类既能够作为属性的client,也能够作为属性提供者的原因。
除了隐藏细节,封装还同意在其它项目中重用同样代码。设计良好的Car 类不限于CarValet 应用程序。还能够在汽车收藏应用程序、零售库存跟踪应用程序,甚至在游戏中使用。随着你开发很多其它应用程序。细致使用封装能够收获一组能够缩短开发周期的即插即用的类。
类不仅限于表示数据;它们还能够实现接口行为、自己定义视图,甚至实现server通信。
2. 点表示法
点表示法同意不用方括号。就可訪问对象信息。能够使用myCar.year取代[myCar year]调用,读取year实例变量的值。虽然这可能看起来像是直接訪问year实例变量。但实际上并不是如此。
属性总是调用方法,而这些方法会訪问对象数据。因为属性依赖于方法将数据带到对象外部。因此并没有破坏对象的封装。
使用my.year会调用[myCar year]。
通过使用属性,编译器会自己主动生成必需的訪问器方法。
假设须要做一些特殊的事情,比如检查远程Webserver。就在.m文件里定义year訪问器方法。然后编译器就会用你写的方法取代自己主动生成的方法。因为方法隐藏,属性简化了代码的显示和布局。比如,能够通过訪问属性,设置表的单元格文字。代码例如以下:
myTableViewCell.textLabel.text = @"Hello World";
而不是下面很笨重的代码:
[[myTableViewCell textLabel] setText:@"Hello World"];
代码的属性版本号更加可读,而且终于更easy维护。
对那些使用点訪问结构的应用程序猿来说,记住点訪问结构是在调用方法而不是遍历对象层次结构是很重要的。
要练习使用点表示法。建议将printCarInfo的实现替换为代码清单2-7中的代码。
代码清单2-7 Car.m 中更新后的printCarInfo 实现 - (void)printCarInfo { if(self.make && self.model) { // 1 NSLog(@"Car Make: %@", self.make); // 2 NSLog(@"Car Model: %@", self.model); NSLog(@"Car Year: %d", self.year); NSLog(@"Number of Gallons in Tank: %0.2f", self.fuelAmount); } else { // 3 NSLog(@"Car undefined: no make or model specified."); } }
代码清单2-7中的关键变化例如以下:
(1) 改动两处变量检查并返回到检查make和model都不为nil。
(2) 使用点标记法,将每一个变量的值打印到日志。
(3) 假设没有make和model。就更新日志。
如今代码清单2-7中的代码更易于阅读,更不用说。此处还在汽车对象没有全然定义的情况下加入了一些打印日志的代码。
并且变量是对象级的元素而不是局部变量。这样显得更清晰。虽然对于这么短的方法,这样做似乎无关紧要。但能够想象。在须要通读更长代码的情况下,这样做的益处。
也能够对初始化方法做类似改动,虽然这是有风险的,尤其是你会使用自己定义訪问器。在自己定义訪问器中。使用点表示法可能是最最危急的——參阅以下的旁注。“为何使用下划线:不用点。不用訪问器。”为何使用下划线:不用点。不用訪问器一种常见的错误来源。就是在属性的自己定义訪问器或可能调用自己定义訪问器的方法中使用点表示法。
举一个简单的演示样例:
- (void) setMake:(NSString*)newMake { if(![newMake isEqualToString:self.make) { self.make = newMake; } }
这段代码是make 属性的自己定义setter。
它检查新的make 值是否与旧的make 值同样,假设不同就,将汽车对象的make 属性设为新值。可是此处有一些隐藏的问题。
self.make = newMake 能够解释为:
[self setMake:newMake];
结果即为对相同setter 方法的递归调用,这个调用又调用相同的setter,一直这样。
这是一个无限循环——当然,更准确说。在应用程序崩溃之前是无限循环。
正确的做法是,在setter 方法中使用下划线版本号的变量。此处赋值变为:
_make = newMake;
因此,安全的做法是,让setter 和getter 使用它们设置或返回的iVar(实例变量)的下划线版本号。不论什么init 方法或自己定义初始化方法能够,也应该使用下划线版本号。
2.3.3 创建并打印Car 对象
眼下在CarValet应用程序中。你已拥有Car类。但它并未在应用程序中的不论什么地方被调用或使用。打开ViewController.m实现文件。并遵循下面步骤:
(1) 在ViewController.m文件的顶部。在最后一条import语句的下方位置,加入#import"Car.h"语句。
(2) 在viewDidLoad方法的下方添加代码清单2-8中的viewWillAppear:方法。
代码清单2-8 ViewController.m 文件里的viewWillAppear:方法 - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; Car *myCar = [[Car alloc] init]; // 1 [myCar printCarInfo]; // 2 myCar.make = @"Ford"; // 3 myCar.model = @"Escape"; myCar.year = 2014; myCar.fuelAmount = 10.0f; [myCar printCarInfo]; // 4 Car *otherCar = [[Car alloc] initWithMake:@"Honda" // 5 model:@"Accord" year:2010 fuelAmount:12.5f]; [otherCar printCarInfo]; // 6 }
方法viewWillAppear:会在ViewController的视图每次即将在屏幕上显示时被调用。此处是创建和调用Car对象的合适地点。以下描写叙述了在代码清单2-8中。带数字凝视的代码行中所发生的事情:
(1) myCar被分配,而且被初始化为Car类的一个实例。
(2) printCarInfo方法被调用,但由于make和model是nil(未初始化),所以会打印汽车对象没有定义的消息。
假设返回看看代码清单2-7,在printCarInfo方法中。你会看到检查make和model不为nil的if语句以及当它们为nil时的结果消息。
(3) make、model、year和fuelAmount被一一设置。
NSString值的双引號前都有个@前缀,浮点值的末尾有个f。
(4) printCarInfo第二次被调用,这一次make和model已设置。因此有关福特汽车的信息会被打印。
(5) 一个新的汽车对象被创建。并使用自己定义初始化方法设置值。
(6) 不像第(2)步中在简单init后调用printCarInfo的情形,这次调用会打印出本田汽车的信息,由于make和model都已被定义。
当执行这段代码时,会在控制台看到例如以下内容:
2013-07-02 08:35:44.267 CarValet[3820:a0b] Car undefined: no make or model specified.
2013-07-02 08:35:44.269 CarValet[3820:a0b] Car Make: Ford
2013-07-02 08:35:44.269 CarValet[3820:a0b] Car Model: Escape
2013-07-02 08:35:44.270 CarValet[3820:a0b] Car Year: 2014
2013-07-02 08:35:44.270 CarValet[3820:a0b] Number of Gallons in Tank: 10.00
2013-07-02 08:35:44.270 CarValet[3820:a0b] Car Make: Honda
2013-07-02 08:35:44.271 CarValet[3820:a0b] Car Model: Accord
2013-07-02 08:35:44.271 CarValet[3820:a0b] Car Year: 2010
2013-07-02 08:35:44.272 CarValet[3820:a0b] Number of Gallons in Tank: 12.50
2.4 属性:另外两个特性
当前版本号的CarValet应用程序是下一章须要的。
本节内容涵盖CarValet项目的两种不同的变化形式。在本章的演示样例代码中也包括了这两种变化形式。
读者中有人可能会怀疑,在汽车对象创建后改动make、model和year是否是个好主意。
到眼下为止,你已经依照属性默认的读写配置使用它们。然而,将属性设置为仅仅读是非常easy的。
全部须要做的就是在声明属性时添加一点额外内容。属性声明的一般形式例如以下:
@property <(qualifier1, qualifier2, ...)> <type> <property_name>;
你已经使用了type(类型)和property_name(属性名称)。汽车对象的make类型为NSString *,属性名称为make。
当中。限定符能让你改动非常多东西。包含将属性设置为仅仅读。并为内存管理指定不同级别的对象全部权,甚至改动默认getter和setter方法的名称。
因此,假设想要不同的行为,仅仅须要改动默认值就可以。
故而。创建仅仅读属性很easy,仅仅须要包括readonly限定符。
想将make、model和year设置为仅仅读,仅仅须要改动.h文件里的定义:
@property (readonly) int year;
@property (readonly) NSString *make;
@property (readonly) NSString *model;
改动成功之后,试着构建这个项目。你会在ViewController.m中得到三个错误,提示你在尝试设置仅仅读属性的值。如今。删除或凝视掉ViewController中设置仅仅读值的这些代码。
可是,假如你想实现这些属性在外部仅仅读。而在内部能够读写。也非常easy,由于能够在.m文件里又一次声明属性。能够通过在Car.m文件的@implementation语句之前添加例如以下代码。添加或覆盖类的接口定义:
@interface Car() @property (readwrite) int year; @property NSString *make; @property NSString *model; @end
Car对象使用新的实例变量定义。注意明白指定readwrite并非必需的,由于readwrite是默认值。
要看到这些代码能执行。能够遵循下面步骤加入一个更新make属性的方法:
(1) 打开Car.h文件。并在printCarInfo:的下方加入例如以下方法:
- (void)shoutMake;
(2) 打开Car.m文件并刚好在printCarInfo:的下方加入例如以下方法:
- (void)shoutMake { self.make = [self.make uppercaseString]; }
(3) 打开ViewController.m而且移除创建和打印第一个myCar对象的方法调用。
(4) 在viewWillAppear:的末尾添加对shoutMake的调用以及对printCarInfo的调用。终于结果看起来例如以下(新代码以粗体显示):
... qfuelAmount:12.5f]; [otherCar printCarInfo]; [otherCar shoutMake]; [otherCar printCarInfo]; }
当执行代码时。最后一次对printCarInfo的调用会以全大写字母显示汽车品牌。能够通过在viewAppear:中设置myCar的make属性,让自己确保一切工作都按预期执行。然而,你将得到错误提示:你正在试图设置仅仅读变量的值。
自己定义getter 和setter
在还有一版本号中,有时你并不想使用编译器生成的默认getter和setter方法。比如。如果你想让self.fuelAmount返回值为公升而不是加仑,但仅当新属性showLiters值为YES时才会如此。
因而,你还会想要通过使用isShowLiters。以訪问这个新属性。
加入这个属性仅仅须要一行代码。在已有属性的下边加入下面代码:
@property (getter = isShowingLiters) BOOL showLiters;
这个限定符为该getter方法设置了一个不同的名称。
你没有使用aCar.showLiters检查这个变量的值,而是使用aCar.isShowingLiters—— 一个更具描写叙述性的名称。设置这个值仍然使用
aCar.showLiters: if(aCar.isShowingLiters) { aCar.showLiters = NO; }
类似的,能够按例如以下方式改动setter方法的名称:
@property (setter = setTheFuelAmountTo:) float fuelAmount;
然而。自己定义的setter与getter方法表现得有点不同。
须要发送一条消息以调用自己定义setter方法。下列语句能运行:
[aCar setTheFuelAmountTo:20.0f];
可是这条语句不能运行:
aCar.setTheFuelAmountTo = 20.f;
原子(atomic)和非原子(nonatomic)
新属性定义后。是时候覆盖fuelAmount的getter方法了。在Car.m文件末尾紧跟printCarInfo:
之后加入以下这些代码:
- (float)fuelAmount { if(self.isShowingLiters) { return (_fuelAmount * 3.7854) ; } return _fuelAmount; }
假设isShowingLiters是true。那么这个自己定义getter方法返回的是公升而不是加仑(当然,这个公式用的是美国加仑而不是英国加仑,可是有关计量标准的世界范围统一的知识。超出了本书范围)。注意此次必须使用下划线版本号的实例变量,以避免无限循环參见2.3.2节的旁注为什么使用下划线:不用点。不用訪问器”。
在加入这种方法之后,你会注意到一个黄色的警告三角形。内容是“writable atomicproperty…”。这是什么?在此处,这个警告是全然正确的。
还有一个属性限定符是变量的原子性。
也就是说,是否在不论什么时刻仅有一个对象能訪问这个变量(原子或同步訪问),还是多个对象能够同一时候在多个线程中訪问这个对象(非原子或非同步訪问)?
当在多线程环境中进行开发时,可以使用atomic属性。确保赋值依照预期运行。当将一个对象设置为atomic时。编译器会在它被訪问或改动之前,加入自己主动为对象加锁的代码,并且在之后加入解锁代码。这就确保了无论是否有并发线程,设置或读取对象的值都可以被完整地运行。
然而,设置原子性(atomic)的成本高得离谱,而且有无限等待解锁的危急。
全部属性默认都是原子的,可是能够使用nonatomic属性限定符。这是通常更加安全且性能更好的替代者:
@property (nonatomic) NSString *make;
将属性设置为nonatomic并不能加速訪问,但可能在两个相互竞争的线程试着同一时候改动同一属性时遇到问题。
原子属性。用加锁/解锁行为。可确保对象从開始到结束得到完整更新,之后才会运行兴许的读取或变更行为,可是原子属性应当仅在须要时才使用。
有人提出,訪问器通常并非加锁的合适地点。并不能确保线程安全。即使全部属性都是原子的。对象也可能会被设置为无效状态。
在实践中,大多数属性会被标记为nonatomic。处理可能的线程安全訪问的问题会用到其他机制。
更深入的讨论可參见Learning Objective C 2.0,第2版,Robert Clair著。
改动Car.h中的fuelAmount属性声明,纠正错误(当改动时,能够顺手为全部属性添加nonatomic限定符,包含showLiters):
@property (nonatomic) float fuelAmount;
如今须要測试上述改动是否正确。重新,仅仅须要在ViewController.m中viewWillAppear:的末尾加入几行代码就可以:
otherCar.showLiters = YES; [otherCar printCarInfo];
当执行这段代码时,你会看到一辆本田车。它的油箱有12.50加仑,然后再次打印为47.32加仑。加仑!?
可是,这是本章末尾挑战题3的内容。
2.5 子类化和继承:挑战一下
随着汽车厂商持续改进使用燃料的方式,客户要求你可以表示一辆混合动力汽车。你的任务是创建继承自Car类的HibridCar类,而且加入一个方法,以返回直到汽车的电池电量和燃料耗尽汽车所跑的里程数。
可以将这种方法命名为-(float)milesUntilEmpty。
线索:
不要忘记跟踪每加仑英里数(Miles Per Gallon,MPG),这样能够计算汽车失去动力前的距离。比如。2013 款丰田普锐斯混合动力汽车能够达到42MPG。假设油箱里剩下10 加仑,那么理论上这辆车在油箱耗尽之前能够行驶402
英里。
思考几分钟。
想到解决方法了吗?
阅读以下的内容。看看怎样做。
继承和子类化
在Objective-C中。每一个新类都派生自已有的类。代码清单2-3到2-7中描写叙述的Car类继承自NSObject——Objective-C类树的根类。每一个子类加入或改动从父类(也称为超类)继承来的状态和行为。
Car类向它所继承的NSObject类中加入了一些实例变量和方法。
HibridCar类继承自Car类并添加了一些功能。根据混合动力汽车可以达到的MPG。计算汽车在燃料耗尽前可以行驶的距离。代码清单2-9和2-10展示了实现HibridCar类的一种可能方法。
本章的演示样例代码在目录“CarValet
HybridCar”中包括代码清单2-9到2-11的项目。
代码清单2-9 HybridCar.h 头文件 // HybridCar.h // CarValet #import "Car.h" @interface HybridCar : Car @property (nonatomic) float milesPerGallon; - (float)milesUntilEmpty; - (id)initWithMake:(NSString *)make model:(NSString *)model year:(int)year fuelAmount :(float)fuelAmount MPG:(float)MPG; @end
首先,注意.h文件有多小。这个文件所须要做的一切,就是指明Car和HybridCar的不同之处——此处。是一个属性和两个方法。
这个属性存储了这辆混合动力汽车所能达到的每加仑英里数。milesUntilEmpty返回这辆汽车使用油箱当前含量(fuelAmount)能够行驶的公里数,自定义初始化方法添加了一个MPG參数以设置milesPerGallon。
代码清单2-10显示了HybridCar类可能的实现文件。 代码清单2-10 HybridCar.m 实现文件 // HybridCar.m // CarValet #import "HybridCar.h" @implementation HybridCar - (id)init { self = [super init] ; if (self != nil) { _milesPerGallon = 0.0f; } return self; } - (id)initWithMake:(NSString *)make model:(NSString *)model year:(int)year fuelAmount:(float)fuelAmount MPG:(float)MPG { self = [super initWithMake:make model:model year:year fuelAmount:fuelAmount]; if(self != nil) { _milesPerGallon = MPG; } return self; } - (void)printCarInfo { [super printCarInfo]; NSLog(@"Miles Per Gallon: %0.2f", self.milesPerGallon); if(self.milesPerGallon > 0.0f) { NSLog(@"Miles until empty: %0.2f", [self milesUntilEmpty]); } } - (float)milesUntilEmpty { return (self.fuelAmount * self.milesPerGallon); } @end
当子类和超类都包括基本初始化方法时,在子类中实现init方法至少有两种主要方法。一种是使用一些默认值以调用子类的基本初始化方法。
方法主体看上去会是这样:
return [self initWithMake:nil model:nil year:1900 fuelAmount:0.0f MPG:0.0f];
然而,这样做会产生隐藏的bug和/或维护成本。如果Car有若干子类,可能有混合动力型、电动型以及柴油型。甚至可能有子类的子类。如GasElectricHybrid、DieselElectricHybrid等。
将生产年份的默认值设置为0,从而可以非常easy地检測到是否存在忘记设置的值。假设每个子类的init方法都使用对应类的自己定义初始化方法。那就不得不改动每一个子类中的值。忘记值的改动就会引入bug。
然而,假设init方法使用[super init],然后设置特定于子类的默认值,那么仅仅须要在一个地方进行改动就可以。
此处有个不错的演示样例,使用的initWithMake:model:year:fuelAmount:MPG:是子类自己定义初始化方法继承超类方法,并添加额外功能——详细地设置了milesPerGallon。首先,调用超类的initWithMake:model:year:fuelAmount:方法,初始化Car对象的属性。然后初始化HybridCar对象详细的值。因为为Car类添加了新的属性。HybridCar详细化了printCarInfo方法。第一件事就是调用超类Car里的同样方法。然后,特定于混合动力汽车的信息被打印出来。
详细化(specialization)是继承中非常强大的一部分,同意每一个类仅仅做它须要做的事情。当详细化与封装结合时,能够让一个类的开发人员仅仅聚焦于那个类,利用继承链上方的公有方法和属性以加速开发过程。
milesUntilEmpty方法用于计算在油箱耗尽前这辆汽车还能再跑多少英里。它使用一个简单的公式,将MPG乘以油箱中燃料的加仑数。
在真实的混合动力汽车中,算法将非常可能复杂得多。
最后一步是往CarValet应用程序的ViewController中添加一个HybridCar类的实例。你须要在ViewController.m文件的顶部加入#import "HybridCar.h"语句。然后将代码清单2-11中的内容加入到viewWillAppear:方法中。
代码清单2-11 加入一辆混合动力汽车到ViewController.m 文件里 HybridCar *myHybrid = [[HybridCar alloc] initWithMake:@"Toyota" model:@"Prius" year:2012 fuelAmount:8.3f MPG:42.0f]; [myHybrid printCarInfo];
myHybrid实例被创建而且用make、model、year和MPG设置了实例变量。混合动力汽车的信息被打印。然后NSLog被调用,显示燃料耗尽之前汽车还能够行驶多少英里。
假设执行CarValet应用程序,就将在调试控制台看到例如以下信息:
2013-07-03 08:39:45.458 CarValet[9186:a0b] Car Make: Toyota
2013-07-03 08:39:45.458 CarValet[9186:a0b] Car Model: Prius
2013-07-03 08:39:45.459 CarValet[9186:a0b] Car Year: 2012
2013-07-03 08:39:45.459 CarValet[9186:a0b] Number of Gallons in Tank: 8.30
2013-07-03 08:44:39.419 CarValet[9346:a0b] Miles Per Gallon: 42.00
2013-07-03 08:44:39.419 CarValet[9346:a0b] Miles until empty: 348.60
HybridCar类能够用多种不同的方式进行定义和实现。花点时间创建一些变化形式。关键是要開始适应Objective-C的语法。能够通过完毕本章结尾的挑战题,进行很多其它练习。
当继续阅读本书,而且继续编写自己的应用程序时。Objective-C的语法和模式会变成你的第二天性。
2.6 小结
本章提供无删节的、大信息量的对Xcode、Objective-C语法、对象、类、属性和继承的介绍,此外还让你使用Xcode练习创建项目,以及Objective-C概念。
本章还是在你通读本书时,能够返回查看的一章。要想获得最大的价值。应该试着直接在Xcode中试验本章中讨论的全部内容。花时间摆弄演示样例应用程序。亲自己主动手获得的经验是获取iOS开发关键技能的最好方法。
学习Objective-C须要的不不过一章。假设要认真学习iOS编程。而且这些概念对你来说还非常生疏。那么请考虑搜寻专门为这个平台的新手介绍这些技术的单一主题书籍。考虑Learning Objective-C 2.0:A
Hands-on Guide to Objectiv-C for Mac and iOS Developers。第2版,Robert Clair著;或者Programming in Objective-C。第5版,Stephen G. Kochan著。对于Xcode,查找Xcode 4 Unleashed,第2版。Fritz
F. Anderson著;或者Xcode 4 Developer Reference。Richard Went著,Richard Went为Xcode 5提供了更新的版本号(虽然Xcode 4版本号也是实用的)。
苹果公司也提供很好的 Objective-C 2.0 简单介绍。 位于http ://developer.apple.com/Mac/library/do
cumentation/Cocoa/Conceptual/ObjectiveC/Introduction/introObjectiveC.html。
在本章。你创建了CarValet应用程序。在下一章,你将在这个项目的基础上构建并学习故事板,这是一种图形化地创建应用程序中全部屏幕的方式。而且将它们组装在一起。
你还将学习很多其它关于Objective-C和iOS编程中一些重要技术的知识。
2.7 挑战题
1. 更新printCarInfo方法。当仅仅有make是nil时打印“Car undefined:no make specified”。当仅仅有model为nil打印“Car undefined:no model specified”。假设两者都为nil。那么仍然打印“Car undefined:no make or model specified”。
能够通过调用initWithMake:model:year:fuelAmount:的变种,创建汽车測试对象以检查代码。
2. 创建Car类的子类ElectricCar。花点时间设计该类:实例变量,对已有方法的改动,以及不论什么独有的方法。当做完这些后,要么開始实现,要么继续阅读以了解一些可能的方式。设计ElectricCar类有若干方式。
一部分选项取决于你想从Car类继承多少东西。在描写叙述汽车的实例变量中。唯独fuel可能是个问题。
电动汽车使用充电器。能够重用fuel而且假装它就是charge(充电器)。那么你须要做的不过改动printCarInfo而且添加一个方法。用于打印剩余电量。还能够添加一个实例变量用于表示每千瓦时的行驶距离,而且使用那个值计算这辆汽车剩余的可行驶里程。
3. 在2.4节的“自己定义getter和setter”部分。能够看到怎样依据fuelAmount返回美国加仑或公升数。
可是printCarInfo总是打印加仑数。改动printCarInfo。使得在isShowingLiters为NO时打印加仑数,为YES时打印公升数。当printCarInfo使用公升时。改动Car类,使得能够用英国加仑、美国加仑和公升打印结果。
你须要找到一种方法,设置燃料用哪种单位进行显示。假设使用BOOL类型,注意有可能多个BOOL变量同一时候被设置为YES。
《iOS开发全然上手——使用iOS 7和Xcode 5开发移动与平板应用》试读电子书免费提供,有须要的留下邮箱,一有空即发送给大家。
别忘啦顶哦!