如果你刚开始接触 iOS 或 Mac OS X 编程,首先要学习一点编程语言 Objective-C 入门知识。Objective-C 并不是一门复杂的语言,经过一段时间的接触,你就能体会到它的优雅。Objective-C 语言实现了严格的面向对象编程。它扩展了标准
ANSI C 语言,增加了定义类和方法的语法。它还推行类和接口的动态扩展性,使任何类都能适配和采用。
如果你已经掌握了 ANSI C 语言,下面的内容能够帮助你掌握 Objective-C 的基本语法。如果你有其他面向对象编程语言的基础,你会发现 Objective-C 中含有大量传统的面向对象概念,例如封装、继承、多态性等。反过来,如果你对 ANSI C 尚不熟悉,我们诚挚建议您在阅读本文之前,先至少阅读一篇关于 C 语言的介绍。
在《The Objective-C Programming Language》中完整讲解了 Objective-C 语言。
关于 Objective-C
Objective-C 语言规定了一系列用来定义类和方法的语法,以及用来推行类和可适应接口的动态扩展的结构。
C 语言的超集
既然是 C 语言的超集,Objective-C 支持所有 C 语言的基本语法。你可以继续按你的习惯书写代码,例如原始类型(int、float 等)、结构、函数、指针,还有过程控制语句例如 if…else 和 for 语句。你可以使用标准 C 语言库,比如 stdlib.h 和 stdio.h 中声明的内容。
Objective-C 在 ANSI C 的基础上增加了如下内容:
- 定义新类的语法规约
- 类和实例方法的规约
- 调用方法的语法(称为消息机制)
- 声明属性并从中合成存取方法的语法
- 静态和动态类型的规约
- 块对象(Block)- 封装起来的代码片段,可以在任何时候被执行
- 对基本语言的扩展,例如协议、范畴类等
如果现在还不明白这些 Objective-C 概念也不必担心。当你继续阅读后面的内容时就会学习这些概念。如果你是初次接触面向对象概念的开发者,可以先把对象想象成一个含有函数的结构。这个比喻不算太离谱,尤其对运行时的实现而言。
Objective-C 的优势
Objective-C 不仅提供了其他面向对象编程语言中的抽象概念和运行机制,而且还是一种非常灵活的语言,这种动态性就是 Objective-C 的最大优势所在。这种动态性可以让应用在运行中(即“运行时”)判断其该有的行为,而不是在编译构建时就固定下来。因此,Objective-C 把应用程序从编译时、连接时的限制中解放出来,并在用户掌握控制权时,更多依赖于运行时的符号解析。Objective-C 的动态性来自三个方面:
- 动态类型 可以让你的代码在运行时判断对象的类型。id 数据类型可以在运行时用任何数据类型来替换。所以,你可以让运行时因素来决定代码中用到的对象是什么类型。动态类型让你的应用更加灵活,这是静态类型做不到的,不过这会让数据的严格统一性降低。
注意:静态类性中的类都是固定种类的(比如 NSString *var),它有自身的优势,实际上用处比动态类型更广泛。打个比方,使用静态类型,编译器就可以完全分析你的代码。这让代码性能和可预知性更高。在其他面向对象编程语言中,动态类型有时被成为弱类型,静态类型被成为强类型。
- 动态绑定 让你的代码在运行时判断需要调用什么方法,而不是编译时。就像动态类型把对象的类型放到运行时再去判断一样,动态绑定把选择调用哪种方法的任务放到了运行时去完成。和其他面向对象语言一样,方法调用和代码并没有在编译时就联结在一起,只有在消息发出时,它们才真正联结。 动态类型和动态绑定的存在使得选择哪个接收者以及调用哪个方法都可以在运行时来决定。用一个画图程序来打比方,它能够定义从父类继承而来的图形类应该怎样归类;你可以直接在某个对象上调用 draw 方法,无需知晓该对象的类以及它绘制自己的具体途径。
- 动态载入 可以让你的程序在运行时添加代码模块以及其他资源。有了动态载入特性,应用可以根据需要加载一系列可执行代码以及资源,而不是在启动时就加载所有组件。这能够大大提高性能。可执行代码中可以含有和程序运行时整合的新类。
类和对象
和其他大部分面向对象编程语言一样,Objective-C 中的类也支持封装数据,以及定义可以在该数据上执行的动作。对象是运行时类的一个实例。在类里声明了的实例变量和方法,它的每个实例都在内存中拥有同样的实例变量,以及指向那些方法的指针。创建一个对象时,你需要经过两个步骤:内存分配(allocation)和初始化(initialization)。
Objective-C 中的类有自己的规范要求,必须包括两个不同的部分:接口(interface)和实现(implementation)。接口部分含有类的声明、实例变量和相关方法的声明。既然是作为 C 语言,通过分别定义头文件和源文件,你就可以将公有声明和具体的实现代码给分离开来。(你可以在实现文件里放一些声明代码,因为有些情况下,它们共同构成一个公用程序里的接口部分。)下表列出了这些文件的后缀以及区别:
后缀、源文件类型:
- .h – 头文件。头文件含有类、类型、函数和常量的声明。
- .m – 源文件。这个后缀的源文件可以同时包含 Objective-C 和 C 语言的代码。
- .mm – 源文件。这个后缀的源文件可以同时包含 C++、Objective-C 以及 C 语言的代码。只有在你的 Objective-C 代码中用到了 C++ 的类或者特性时才需要使用这个后缀。
如果需要在源代码中包含头文件,你需要使用 #import 命令,和 C 语言中的 #include 命令类似。两者的区别在于,#import 能够保证头文件只被包含一次。
图 1 中是一段的类声明的语法展示,声明了一个叫做 MyClass 的类,它继承于基本类(或根类):NSObject。(根类可以被所有的其他类直接或间接继承。)类声明开头是一条编译器指令 @interface,结尾是一条 @end 指令。在类名称后边(中间用冒号分隔),是父类的名称。在 Objective-C 中,每个类只能有一个父类。类中包含的实例变量(有时被称为 ivar,在其他编程语言中有时被称为成员变量)的声明被一个花括号({ 和 })包裹起来。实例变量是可选的。在实例变量语句块下边是属性(图中没有写出来)和类的方法声明。每个实例变量和方法声明的语句结尾都要有一个分号。
图 1 一段类声明
类的实现的语法也是类似的。开头是编译器指令 @implementation(后面有类的名称),结尾是 @end 指令。方法的实现代码就在这两个指令的中间。实现代码中必须导入它的接口文件,写在代码的第一行。
#import “MyClass.h”
@implementation MyClass
- (id)initWithString:(NSString *)aName
{
// 在这里书写代码
}+ (MyClass *)myClassWithString:(NSString *)aName
{
// 在这里书写代码
}
@end
我们之前讲过,Objective-C 支持包含对象的动态类型变量,它同时也支持静态类性。静态类型变量的声明前边要有所述类的名称。而动态类型变量声明以 id 来代表对象。在某些情形下,你会用到动态类型变量。比如,一个数组这样的对象集合(里面包含的对象类型可能是无法预知的)就会用到动态类型变量。这样的变量能够提供无比灵活的功能,使得 Objective-C 程序能够拥有更大的动态性。
下面的例子展示了静态类型和动态类型变量的声明方式:
MyClass *myObject1; // 静态类性
id myObject2; // 动态类型
NSString *userName; // 曾出现在“你的第一个 iOS 应用”中(静态类型)
请注意第一个声明里的 * 星号。在 Objective-C 语言中,对象永远是通过指针来引用的。如果现在你还不能明白这句话的意思也不必担心,在学会Objective-C 基础之后再研究指针也不迟。现在你需要记住的,是在静态类型变量声明时,变量名称前面一定要有 * 星号。而 id 类型则暗示它是一个指针。
方法和消息
如果你刚刚开始接触面向对象编程,不妨先把“方法”想象成每个对象特有的一个函数。通过向一个对象发送消息,你便调用了对象的某个方法。Objective-C 中有两种方法:实例方法以及类方法。
- 实例方法顾名思义,它的作用域仅在某个类的一个实例当中。换句话说,在调用某个实例方法之前,你必须先创建一个实例才行。实例方法是最常见的方法。
- 类方法则是指其作用域包括该方法所在的整个类。它不要求某个对象的实例作为消息的接收者。
方法的声明由以下几个部分构成:方法类型标识符,返回类型,一个或多个方法签名关键字,以及参数类型和名称。下面的图中是实例方法 insertObject:atIndex: 的声明语句。
在实例方法中,声明的开头是一个 – 减号;而声明类方法时前面要使用 + 加号。下文的“类方法”章节将详细讲述类方法的概念。
方法的名称(insertObject:atIndex:)是一系列方法签名关键词联结而成,包括冒号。冒号表示将会出现参数。在上面的例子中,这个方法含有两个参数。如果某个方法没有参数,则需要将第一个(也是唯一一个)方法签名关键词后面的冒号省略掉。
当你需要调用一个方法时,就是要向实现了该方法的对象发送一个消息,简而言之,就是给对象发送消息。(虽然“发送消息”常常用作“调用方法”的近义词,但是在 Objective-C 的运行时中,实际情况是发送消息。)一个消息就是一个方法的名字带上该方法所需的参数信息(要和数据类型正确对应)。你向对象发送的所有消息都是动态调度的,以此来实现 Objective-C 语言的多态性。(多态性是指:不同类型的对象都能对同一种消息做出回应。)有时,所调用的方法是由接收消息的对象的类的父类实现的。
要调度一个消息,运行时要求正确的消息表达方式。消息表达式由一对方括号([ 和 ])把消息(以及所需的参数)包裹起来,接收消息的对象写在左边括号后边。比如,要向 myArray 变量所含的对象发送一个 insertObject:atIndex: 消息,你要按下面的语法进行书写:
[myArray insertObject:anObject atIndex:0];
为了避免声明大量局部变量来存储临时结果,Objective-C 允许嵌套消息表达式。每个嵌套的表达式返回的值都会作为一个参数,或者接收消息的对象,甚至是另一个消息。比如,你可以将上一个例子中的任意一个变量替换成用消息接收数值。这样一来,如果你还有一个叫做 myAppObject 的对象,它含有访问数组对象以及将对象插入数组的方法,你可以将那个例子改造成下面这样:
[[myAppObject theArray] insertObject:[myAppObject objectToInsert] atIndex:0];
Objective-C 还提供了点语法特性,用来访问存取方法。存取方法是对象的 get 和 set 语句,这里是封装的关键,封装是所有对象的重要特性。对象把自己的状态封装(或隐藏)起来,并提供一个能让所有实例访问这个状态的通用接口。利用点语法,之前的例子又可以被改写成:
[myAppObject.theArray insertObject:myAppObject.objectToInsert atIndex:0];
点语法还可以用来赋值:
myAppObject.theArray = aNewArray;
这个语法其实是 [myAppObject setTheArray:aNewArray]; 这个语句的另一种表述方式。
而且,请回想一下,在“你的第一个 iOS 应用”教程里,你已经用过点语法来对变量进行赋值了:
self.userName = self.textField.text;
下文中的“已声明的属性和存取方法”章节将向你详细介绍存取方法。
类方法
虽然之前的范例都是向类的实例发送消息,但你也可以向类自身发送消息。(这里的类,可以被理解为由运行时生成的 类 的对象。)向一个类发送消息时,该方法必须在之前被声明为类方法,而不是实例方法。类方法和 C++ 中的静态方法很相似。
你通常会将类方法用作工厂方法,借以创建该类的新的实例或者访问与该类相关的某些共有信息。类方法的声明语法和实例方法的十分相像,不同之处是方法类型标识符是一个 + 加号,而不是 – 减号。
下面的例子展示了如何把一个类方法当作该类的工厂方法进行调用。在本例中,array 方法是 NSArray 类的一个类方法,并且被 NSMutableArray 类继承。它会给这个类的新实例分配内存并将这个实例初始化,最后把它返回给你的代码。
NSMutableArray *myArray = nil; // nil 本质上等同于 NULL
// 创建一个新数组,并将其赋值给 myArray 变量。
myArray = [NSMutableArray array];
已声明的属性和存取方法
一个属性,按通常的理解就是对象封装的状态里的一项。它要么是一个特性,比如名字或者颜色;要么是与另一个或多个其他对象的关联。对象的类定义了一个接口,使用该对象的用户可以获取(get)和设置(set)封装属性中的数值。而执行这个功能的方法就叫做存取方法。
存取方法共有两种,且都要符合一套命名规约。“Getter(取值器)”存取方法能够返回某个属性的值,它的方法名和该属性同名。“Setter(赋值器)”存取方法能够给某个属性赋予新的值,它的命名规约是 set属性名称: 这样的形式,属性名称的首字母大写。在 Objective-C 框架中,只有严格按照规约对存取方法进行命名才能实现多种技术。
Objective-C 提供了已声明的属性,可以作为声明的便利途径,有时还能作为存取方法的实现。在“你的第一个 iOS 应用”教程中,你曾经声明了 userName 属性:
@property (nonatomic, copy) NSString *userName;
已声明的属性使得 getter 和 setter 方法无需在类里显式声明。相反,你在声明属性时可以决定其具体行为方式,然后要求编译器基于属性声明,生成(或说创建)实际可用的 getter 和 setter 方法。已声明的属性减少了大量不必要的代码,节省开发者的时间,并且让你的代码更加清爽、减少出错的可能。
在类接口文件中,要包含属性声明和方法声明。基本的声明要使用 @property 编译器指令,后面是属性的类型和名称。你还可以为属性设定不同的选项,也就是说可以调整存取方法的具体行为方式,比如属性是否为弱引用,或者是否为只读属性。这些选项写在 @property 指令后边的圆括号中。
下边的几行代码展示了更多属性声明的例子:
@property BOOL flag; // 默认是只声明类型和名称
@property (copy) NSString *nameObject; // 在赋值过程中拷贝对象
@property (readonly) UIView *rootView; // 只声明 getter 方法
在类的实现代码中,你要使用 @synthesize 编译器指令来要求编译器根据声明的情况,生成存取方法:
@synthesize flag;
@synthesize nameObject;
@synthesize rootView;
你也可以把 @synthesize 语句放到一行里面:
@synthesize flag, nameObject, rootView;
在 @synthesize 指令中,你还可以命令编译器添加相应的实例变量到类定义中去。要增加一个实例变量,在属性的名称后面写一个等号,然后写你想要的实例变量名称:
@synthesize nameObject=_nameObject;
块对象
块对象是封装了一系列功能的一个对象,或者更通俗地讲,它是一个代码片段,能够在任何时刻被执行。它们本质上就是可移植的匿名的函数,可以作为其他方法的参数传入,也可以作为其他方法或函数的返回值。块对象本身有含类型的参数表,可能带有不确定的或已声明的返回值类型。你可以把一个块对象赋值给某个变量,然后在你需要的时候像调用函数一样调用它就行了。
脱字符(^)是块的语法标记。还有按照我们熟悉的参数语法规约所定义的返回值以及块对象的主体(也就是可以执行的代码)。下图是如何把块对象赋值给一个变量的语法讲解:
接下来,按照调用函数的方式调用块对象变量就可以了:
int result = myBlock(4); // 结果是 28
块对象可以在局部作用域之内共享数据。块对象的这个特性非常有用,假设你实现了一个方法,该方法定义了一个块对象,那么这个块对象就可以访问该方法内的局部变量和参数(包括堆栈变量),也可以访问函数和全局变量,甚至包括实例变量。这种访问是只读性质的,但如果变量声明使用了 __block 修饰符,则它的值可以在块对象中被改变。即使在方法或函数封装的块对象返回一个值并销毁其作用域之后,只要对该块对象的引用不消失,局部变量作为块对象的一部分将一直存在。
和方法、函数的参数类似,块对象可以被当作一个回调函数。当方法或函数被调用时,它们会执行某些功能,并在合适的时机回调之前调用它们的代码(通过块对象),来请求附加信息,或者从中获取程序特定的行为。块对象让调用者能够在调用的时候提供回调代码。块对象不会将请求数据打包到一个“上下文”结构中,而是从方法或函数的相同作用域中捕获数据。由于块对象代码无需在单独的方法或函数中另外实现,所以你的实现代码能够变得更加简洁,可读性更强。
Objective-C 框架有许多带有块对象参数的方法。比如这段,在 UIKit 框架里声明了如下类方法,它有两个块对象参数:
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion;
这个方法可以让你生成一个动画效果。第一个块对象参数用来选择动画效果;第二个块对象参数指定动画完成后要执行的任务。下面的例子中,第一个块对象将视图动画的结尾 alpha 值设为 0(让它变成透明的)。第二个块对象则把整个视图移除。
[UIView animateWithDuration:0.2 animations:^{
view.alpha = 0.0;} completion: ^(BOOL finished) {
if (finished == YES)
[view removeFromSuperview];
}];
协议和范畴类
协议可以用来声明能够在任何类中实现的方法,甚至那些实现该方法的类继承自别的类。协议方法定义的行为是独立于任何一个类的。协议可以定义一个要求其他类必须承诺实现的接口。也就是说,如果你的类实现了协议中的方法,那么这个类就承诺要完成该协议。
从实用的角度说来,协议定义了一系列方法,并建立起对象之间的“合约”。而这些对象不必是任何一个确定的类的实例。这个合约使得对象之间可以进行交流。某个对象想要告诉另一个对象,马上将要面临的事件,或者想要询问关于那些事件的建议。
UIApplication 类实现了所需的应用行为。你不必为了接收简单的应用当前状态的消息而创建一个 UIApplication 的子类。UIApplication 类会调用指定的委托对象中的特定方法来传递那些消息。实现了 UIApplicationDelegate 协议方法的对象就能够接收到那些消息了,并且能够提供合适的反馈。
在承诺实现、或采用某个协议的接口代码中,协议的名称要写在父类名称后边的一对尖括号里(<…>)。在“你的第一个 iOS 应用”教程里,你采用了 UITextFieldDelegate 协议:
@interface HelloWorldViewController : UIViewController <UITextFieldDelegate> {
}
@end
在实现中,你无需声明协议方法。
协议的声明看起来和类接口很相似,不过不同的是协议没有父类,并且不含任何实例变量(但它们能够声明属性)。下面的例子展示如何声明只有一个方法的简单协议:
@protocol MyProtocol
- (void)myProtocolMethod;
@end
对于许多委托协议而言,采用一个协议就等于是实现该协议中定义的方法。有些协议要求你明确声明你会支持该协议,而有些协议则是既包含必须实现的方法,也包含可选方法。
当你在 Objective-C 框架中浏览头文件时,你很快就会看到类似这样的语句:
@interface NSDate (NSDateCreation)
这行语句声明了一个范畴类(category),其语法是将范畴类的名称包裹在一对圆括号中。范畴类是 Objective-C 语言的一个特性,让你能够扩展某个类的接口,而无需创建它的子类。范畴类中的方法将成为此类的一部分(在你的程序作用域范围内),并会被此类的所有子类继承。你可以向此类(或它的子类)的任意一个实例发送消息来调用范畴类中声明的方法。
你可以利用范畴类在一个头文件里组织互相关联的方法声明。你甚至可以在不同的头文件中放入不同的范畴类声明。Cocoa Touch 框架和 Cocoa 框架在几乎所有头文件中都利用了这个技术,代码才如此明晰。你还能使用匿名范畴类(也就是在圆括号中不写任何字符),这样可以把实例变量隐藏在私有的实现文件里。
预定义类型和编码策略
在 Objective-C 语言中有一些特定的词汇,你要避免在声明变量时使用这些词汇,因为它们都有专门的用途。其中有一些是编译器指令,以 @ 符号开头(例如 @interface 和 @end)。有些保留词汇是预定义类型,以及和这些类型有关的文字。Objective-C 使用一系列不属于 ANSI C 的预定义类型和词汇。在某些情况下,这些类型和词汇会代替它们在 ANSI C 中的对应者。下表列出了几个非常重要的类型,包括每个类型所允许的词汇。
类型、描述和词汇:
id – 动态对象类型。动态类型和静态类型对象的否定词汇为 nil。
Class – 动态类的类型。它的否定词汇为 Nil。
SEL – 选择器的数据类型(typedef);这种数据类型代表运行时的一种签名方法。它的否定词汇为 NULL。
BOOL – 布尔型。代表它的值的词汇为 YES 和 NO。
你通常会在代码排错以及流程控制中使用这些预定义的类型和词汇。在程序的流程控制语句中,你可以通过检测特定词汇来判断如何采取下一步动作。例如:
NSDate *dateOfHire = [employee dateOfHire];
if (dateOfHire != nil) {
// 处理此种情况
}
把代码解释一下:如果对象代表的聘用日期不为 nil,也就是说是一个合法的对象,那么逻辑就朝一个特定的方向发展。下边是这段代码的简化版,效果是相同的:
NSDate *dateOfHire = [employee dateOfHire];
if (dateOfHire) {
// 处理此种情况
}
你甚至可以把代码简化成这个样子(前提是你不需要引用 dateOfHire 对象):
if ([employee dateOfHire]) {
// 处理此种情况
}
处理布尔值的方法是一样的。在这个例子中,isEqual: 方法会返回一个布尔型的值:
BOOL equal = [objectA isEqual:objectB];
if (equal == YES) {
// 处理此种情况
}
你同样可以按照前边省略 nil 的方式来简化这段代码。
在 Objective-C 语言里,你可以对 nil 发送消息,不必担心任何副作用。其实,是根本不会有任何作用,只不过运行时会返回 nil,如果方法本来要返回一个对象的话。发送给 nil 的消息返回的值只要是一个对象,代码就能继续正常工作。
另外两个重要的预留词汇是 self 和 super。前者 self 是个局部变量,你可以在消息实现中把它看做当前对象进行引用;它和 C++ 语言中的 this 是一样的。你可以用预留词汇 super 替换 self,但只能作为消息表达式的接收者。如果你对 self 发送了消息,那么运行时首先就会在这个对象的类中寻找相应的方法实现;如果这里找不到,那么它会转而到其父类中去查找(如此往复)。如果你对 super 发送消息,运行时首先就是去父类中寻找方法的实现。
利用 self 和 super 主要是为了发送消息。当被调用的方法在 self 的类中被实现之后,你就可以向 self 发送消息,例如:
[self doSomeWork];
self 还可以用在点语法中,用来调用由已声明的属性生成的存取方法,例如:
NSString *theName = self.name;
你常常会在重载(既重新实现已有的方法)从父类继承而来的方法时向 super 发送消息。在这种情况下,被调用的方法拥有和重载后方法的签名相同。
注:本《iOS应用开发入门指南》译自苹果官方 iOS 开发者资源库中的 Start Developing
iOS Apps Today 系列教程。