《iOS开发完全上手——使用iOS 7和Xcode 5开发移动与平板应用》之Objective-C新手训练营

移动开发 专栏收录该内容
33 篇文章 0 订阅
     编写Hello World应用程序通常被认为,是学习任何编程语言的第一步。在这一章,你将创建iOS版的Hello World应用程序作为起步,快速了解Xcode这个开发iOS应用程序的主要工具。
     下一步,你将学习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-1 Xcode 的欢迎页面

   在图2-2所示的模板选择窗口中,选择左边的iOS菜单下的Application,然后选择SingleView Application(单视图应用程序),单击Next按钮。


图2-2 Xcode 模板选择窗口

警告:截图和功能可能会有所不同
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) 单击Next按钮。


图2-3 Xcode 项目选项面板
     Class Prefix(类前缀)可以让你指定包含在类名中的前缀,Xcode会在定义类时,自动将其添加在类名前。使用前缀是一种好的方式,能有效地避免类名与现有或将来的iOS系统类发生命名冲突。例如,如果创建了类SlideMenuViewController,而某次iOS系统更新时增加了一个同名的类,那么至多可能是应用程序无法编译。更糟糕的情况是,应用程序说创建的实例是同名系统类的实例,不是自己所写的类的实例,这会带来非常糟糕的bug。但如果指定前缀为“MS”,那么Xcode会创建名为MSSlideMenuController的文件和类,从而避免潜在的冲突。
     在本书中,为了让命名更简短,并未使用前缀,但是你应当根据自己的开发者名称或公司名称设置前缀。
专家提示:前缀格式
最典型的类前缀是一小串大写字母(苹果公司推荐使用3 个字母),通常是你或你公司名称的单词首字母。在公司内部项目或开源项目中,这种格式很常见,但可读性并不是最好。以下是一个滑动菜单类的各种不同的名称,这并不是我实际编写过的一个类,是我想出来的。但不妨试着观察一下列表中的这些类名条目,按照你浏览代码时的方式,看看哪一个更容易快速识别:
SlideMenuViewController
MSSlideMenuViewController
MS_SlideMenuViewController
msSlideMenuViewController
ms_slideMenuViewController

极其可能的是,全部大写且没有分隔符的版本最难识别。小写的稍稍容易点,但难以识别的程度仅次于前者,更容易识别的是那些带有分隔符的版本。你应当选择能与符号名称简易搭配使用的类前缀。长远来看,你将节省大量的开发与调试时间。


项目选项面板中的其他条目包括以下这些:
• Product Name (产品名称)——应用程序的名称,它会被显示到设备屏幕上。如你在第15 章“部署应用程序”中将要看到的,后续还可以对其修改。
• Organization Name (组织名称)——作为自动生成的源文件头部注释的一部分,用作版权属性。
• Devices(设备)——应用程序的目标设备平台,可以是iPhone、iPad 或Universal(通用应用程序)——可以同时运行在这两种平台上的应用程序。

      上一个Xcode窗口面板询问你Hello World项目的存储位置。如果愿意的话,Xcode还可以为项目创建本地的git仓库。针对当前这个Hello World示例,将Source Control(源码控制)复选框保持为未选中状态。选择一个文件夹并单击创建,如图2-4所示。

     在单击Create按钮之后,你应该能看到在Xcode中,自己的Hello World新项目已被打开,它带有一些Xcode自动生成的文件。至此,你得到一个看似不怎么有趣,但是功能完备的应用程序。如果立刻单击Run(运行)按钮,你的app会显示一个顶部带有状态条的白色屏幕。


图2-4 Xcode 项目文件夹位置面板
2.1.2 Xcode 项目界面快速导航

      开发应用程序时,你将在Xcode中花费大量时间。图2-5显示了展开项目文件并选择ViewController.m文件后的Xcode界面。


图2-5 Xcode 项目界面的组成部分

     以下是对Xcode界面的快速导航。接下来的几章将探讨Xcode界面中不同组件的更多细节知识。下面的数字与图2-5中显示的数字一一对应:

(1) 单击Run按钮(在左边),编译应用程序,并在模拟器或设备中启动应用程序。单击并保持(长按)Run按钮,你会看到额外的选项,比如用于测试或分析项目的选项。右侧的按钮可用于停止正在运行的应用程序、正在执行的构建或已发起的其他动作。
(2) 在这个分为两段的弹出式菜单按钮中,左段用于选择和编辑方案(Scheme),也就是关于你想运行什么以及如何运行该应用程序。右段用于选择在哪儿运行应用程序:是在某种模拟器中还是在某个已连接的设备上。
(3) Status(状态)区域向你展示上一动作的执行结果或当前动作的进度。动作包括构建应用程序、运行应用程序或是下载应用程序到设备上。
(4) Editor(编辑器)按钮用于配置编辑器(图2-5中标为7的区域)中显示的内容。左侧按钮在图中已被选中,只显示一件东西,即主编辑区。中间的Assistant(辅助编辑)按钮将编辑区划分为两块,在右侧会显示与主编辑器相关的文件,在右侧通常情况下会显示头文件。最后一个按钮用于显示源代码差异,并且可以查看文件的不同版本。这一点在使用源代码仓库跟踪代码变动时非常有用。注意,不能同时显示辅助编辑和源代码视图。
(5) 这个区域控制Xcode中工作区视图的总体布局。第一个按钮显示或隐藏Navigation(导航器,图2-5中的区域6)。第二个按钮控制Debugger(调试器,区域10)。最后一个按钮显示Utilities(区域8和9)。
(6) Navigator(导航器)可以显示项目的不同视图。顶部的图标控制视图的类型。在图2-5中,被选中的是文件夹图标,因此这个区域显示的是文件以及基于组(group)的项目导航器。其他图标是:项目符号导航器,用于在全项目范围内进行搜索的放大镜图标,用于查看构建问题的警告三角形图标,还有单元格测试导航器,调试导航器,断点列表,最后是日志视图。容易混淆的一点是,文件和组导航器中显示的文件结构和Finder中的文件结构,这两者可以是不同的。在导航器中,组被显示为文件夹图标,但这些与Finder中的文件夹是不同的。如果想让组和Finder文件夹保持一致,就需要在Finder中添加这些文件夹,并将源文件保存在正确的位置。
(7) Editor(编辑器)是你将在Xcode中花费大多数时间的地方。目前,它显示的是源码编辑器。它还可以显示Interface Builder(界面生成器)、数据模型编辑器或运行时调试测量仪器。
(8) 工具区域显示了不同种类的工具,包括文件信息、快速帮助(当前被选中的内容)、数据模型细节查看器、视图属性(比如颜色、按钮标题以及尺寸,包括屏幕大小和约束)。
(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并没有打开。这时你要检查并确定没有单击显示源代码差异的第三个按钮(参见图2-5中的区域4)。如果源代码控制按钮未选中,尝试选择一个.m或.h文件,然后再次选择故事板文件。如果还是不管用,那么退出Xcode并重启。
(3) 在右下方的查找面板中,输入label并将这个标签对象拖曳到故事板的画布中。
(4) 在Xcode工作区左侧在Utility区域中,在Attributes检查器的文本框内输入Hello World,将这个标签调整得好看一些。图2-6显示这个标签在视图控制器(view controller)的顶部水平居中。

(5) 单击Run按钮,编译并运行Hello World应用程序。


图2-6 通过Xcode 的界面生成器添加Hello World 标签
      当运行Hello World应用程序时,可以看到iPhone模拟器,记得使用方案弹出菜单,将运行的目的地设置为iPhone模拟器。
这就是创建简单Hello World应用程序所需的所有步骤。你甚至不需要编写任何代码。在下一节,你将学习一些Objective-C基础知识,回到这个Hello World应用程序,并添加一些代码以练习Objective-C。

图2-7 运行在iPhone 模拟器中的Hello World 应用程序
使模拟器适应你的屏幕

图2-7 所示的模拟器是全尺寸的、4 英寸Retina 显示屏,并且可能非常大,特别是当在笔记本电脑屏幕上工作时。可以使用Simulator Window | Scale 菜单更改显示屏的显示大小。


2.2 Objective-C 新兵训练营

要成为熟练的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,这本书涵盖了更高级的知识。


2.2.1 Objective-C 编程语言
      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 的消息传递变得容易阅读和理解。在C 语言和其他语言中,你不得不时常参考函数定义以确定每个参数是什么以及参数的顺序。在Objective-C 中,这些都很清晰,就在你面前。在使用一些含有5 个或更多个参数的UIKit 方法调用时,你会特别明显地体会到这一点。


在Objective-C中,方法的参数由冒号(:)字符进行分隔,在参数值之间带有方法名称中的一部分。你的方法可能返回一个值或对象,同样C语言也可以返回一个值。在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-8 Objective-C 方法调用的组成部分
      除了让参数更清晰,方法还引入了一个更加强大的特性。方法可以访问类中定义的所有东西。换句话说,可以访问实例变量以及任意类实例中实现的方法。在这种意义上,方法如何运行对于调用者对象是透明的。某个特定方法的实现代码甚至是整个类可以完全改变而不需要将别的任何地方修改。这在升级或替换应用程序中的特性时是非常有用的:可能是让它们更有效地更新新的硬件特性,甚至彻底替换通信的处理方式。


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中,@符号用在特定的一些关键字中。此处展示的两个元素(@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. 指向对象
      你已经了解到,使用类创建对象是非常简单的事情。当拥有一个新创建(分配)和初始化的对象时,下一步就是引用并使用这个新对象。在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项目时填写的内容。如果需要的话可以修改这些内容,然后单击Next按钮。
(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所示,然后单击Next按钮。


图2-9 在Xcode 中将Car 类的文件类型设置为Objective-C
(3) 在接下来的面板中,在Class文本框中输入Car,在Subclass of文本框中输入NSObject,如图2-10所示。正在创建的是Car类,继承自NSObject。单击Next按钮后会出现保存面板。
(4) 保存面板的底部有一块区域可用于指定Target成员。此时,重要的是确保CarValet被选中,如图2-11所示。在确认该复选框已被选中后,单击Create,Xcode会创建Car类,并且将它放到你的项目中。

图2-10 设置类名为Car 并继承自NSObject


图2-11 在Xcode 中设置Car 类的Target
      在Car类创建后,Xcode会自动打开Car.m实现文件。此时,你需要切换到头文件中,并将 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:

      每个参数表示在警告框中显示或可选显示的元素,包括标题、消息、Cancel按钮文字以 及其他按钮的文字。另一个参数表示delegate(委托对象)。

      使用委托对象是iOS中的另一常见模式,它提供了一种方法,通过让两个对象使用定义好的一组消息(称为protocal(协议)来进行通信。这些消息是协议提供者和委托对象之间的一种契约。本质上,委托对象承诺实现这些消息,而提供者承诺正确地使用它们。协议与类是分开定义的;它们是不同的事物。任何类可以选择以委托对象或实现者的身份使用协议。UIAlert采用这种模式以通知委托对象,用户单击了哪个按钮。当阅读这本书时,你会看到系统对象中更加复杂地使用委托模式的示例。你还会在自己构建的一些对象里实现这种模式——也就是创建协议并向对象添加委托对象代码。


提示:在头文件和实现文件之间切换

在Xcode 中,组合键Ctrl+Cmd+向上箭头可以让你移到下一个配对文件,而Ctrl+Cmd+向下箭头可以让你移到前一个配对文件。这对组合键使得在头文件和实现文件之间切换变得非常简单。


2.3.1 实现Car 方法
      头文件和实现文件共同存储了一个类的实现以及在应用程序其他部分使用该类所需的所有信息。实现的源代码通常包含在.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 sent to 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)变量或信息的方法。编译器还可以生成下划线版本的变量。声明属性是很简单的:
@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. 封装
      封装允许在应用程序中的其他部分,包括任何使用对象的客户端,隐藏实现细节。对象 的内部表示(实例变量)和行为(方法等)与对象向外界声明自身的方式隔离。只要公开声明的细 节保持不变,就可以自由地彻底修改内部实现。属性提供的是,以一种结构良好并且受限地 暴露对象状态和其他信息的方式。
      然而,属性并不限于用作公有变量。它们在类的定义中也可发挥重要作用。属性允许向 类定义的其他部分添加时尚的前瞻性开发技术,包括延迟初始化(lazy loading)和缓存。这就 是类既可以作为属性的客户端,也可以作为属性提供者的原因。

      除了隐藏细节,封装还允许在其他项目中重用相同代码。设计良好的Car 类不限于 CarValet 应用程序,还可以在汽车收藏应用程序、零售库存跟踪应用程序,甚至在游戏中使 用。随着你开发更多应用程序,仔细使用封装可以收获一组可以缩短开发周期的即插即用的 类。类不仅限于表示数据;它们还可以实现接口行为、自定义视图,甚至实现服务器通信。
2. 点表示法
     点表示法允许不用方括号,就可访问对象信息。可以使用myCar.year代替[myCar year]调 用,读取year实例变量的值。 尽管这可能看起来像是直接访问year实例变量,但实际上并非如此。属性总是调用方法, 而这些方法会访问对象数据。由于属性依赖于方法将数据带到对象外部,因此并没有破坏对 象的封装。
      使用my.year会调用[myCar year]。通过使用属性,编译器会自动生成必需的访问器方法。
      如果需要做一些特殊的事情,例如检查远程Web服务器,就在.m文件中定义year访问器方法, 然后编译器就会用你写的方法代替自动生成的方法。 由于方法隐藏,属性简化了代码的显示和布局。例如,可以通过访问属性,设置表的单 元格文字,代码如下:
myTableViewCell.textLabel.text = @"Hello World";

而不是以下非常笨重的代码:
[[myTableViewCell textLabel] setText:@"Hello World"];

      代码的属性版本更加可读,并且最终更容易维护。对那些使用点访问结构的应用程序员 来说,记住点访问结构是在调用方法而不是遍历对象层次结构是非常重要的。 要练习使用点表示法,建议将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是否是个好主意。到目前为止,你已经按照属性默认的读写配置使用它们。然而,将属性设置为只读是很容易的。所有需要做的就是在声明属性时增加一点额外内容。属性声明的一般形式如下:

@property <(qualifier1, qualifier2, ...)> <type> <property_name>;

      你已经使用了type(类型)和property_name(属性名称)。汽车对象的make类型为NSString *, 属性名称为make。其中,限定符能让你修改很多东西,包括将属性设置为只读,并为内存管 理指定不同级别的对象所有权,甚至修改默认getter和setter方法的名称。因此,如果想要不同 的行为,只需要修改默认值即可。
       故而,创建只读属性非常简单,只需要包含readonly限定符。想将make、model和year设 置为只读,只需要修改.h文件中的定义:
@property (readonly) int year;
@property (readonly) NSString *make;
@property (readonly) NSString *model;

      修改成功之后,试着构建这个项目,你会在ViewController.m中得到三个错误,提示你在 尝试设置只读属性的值。现在,删除或注释掉ViewController中设置只读值的这些代码。
      但是,假如你想实现这些属性在外部只读,而在内部可以读写。也很简单,因为可以在.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 atomic property…”。这是什么?在此处,这个警告是完全正确的。另一个属性限定符是变量的原子 性。也就是说,是否在任何时刻仅有一个对象能访问这个变量(原子或同步访问),还是多个 对象可以同时在多个线程中访问这个对象(非原子或非同步访问)?
      当在多线程环境中进行开发时,可以使用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,从而能够很容易地检测到是否存在忘记设置的值。如果每个子类的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开发移动与平板应用》试读电子书免费提供,有需要的留下邮箱,一有空即发送给大家。 别忘啦顶哦!

微信:qinghuashuyou  
更多最新图书请点击查看哦
互动出版网china-pub购书地址:http://product.china-pub.com/3770438


  • 4
    点赞
  • 7
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值