此文概括介绍了 Objective-C 语言中的基本元素。其范围仅限于 Objective-C 语言对 ANSI C 的扩展,而没有介绍 C 语言的基础元素,因为介绍这方面的资料已经有很多了。
A.1 变量
在 Objective-C 中,变量名的词法规则与 ANSI C 中变量名的词法规则相同,即变量名由字母、下划线或数字(1~9)的组合组成,但第一字符不能是数字,而且区分大小写。
A.1.1 Objective-C 保留字
在一门编程语言中,保留字(reserved word)是指在该语言中具有特殊意义的符号,因此它不能用作变量的名称。在 ANSI C 相比的基础上,Objective-C 增加了许多保留字,如表A-1所示:
表A-1 不能用作变量名的 Objective-C 保留字
_Bool | _cmd | _Complex | _Imaginary | atomic |
---|---|---|---|---|
bycopy | byref | BOOL | Class | id |
IMP | inline | inout | instancetype | out |
NO | nonatomic | NULL | SEL | self |
Protocol | restrict | retain | Class | id |
super | YES |
A.1.2 变量的范围
变量的可见性和可访问性与其范围有关。变量可以存在于语句块、函数、方法、对象实例(或关联起来的对象)、单个文件和多个文件中。ANSI C 定义了以下4种变量范围。
- 块: 在语句块中声明的变量拥有块范围。在语句块中声明的变量只能在这个语句块中可见和被访问。
- 文件:拥有文件范围的变量只能在声明它的文件中可见和被访问。
- 函数:在函数/方法中声明的变量拥有函数范围。这些变量只能在相关函数/方法中可见和被访问。
- 函数原型:在函数/方法原型的参数列表中定义的变量拥有函数原型范围。这些变量可以在相关函数/方法中可见和被访问。
访问修饰符
Objective-C 增加一系列访问修饰符指令,使用这些指令可以更精细地控制对实例变量的访问。
Objective-C 定义了多条编译指令,使用这些指令可以控制实例变量的范围,即在程序中控制变量的可见性。
- @private: 实例变量只能在声明它的类和该类的其他实例中被访问。
- @protected: 实例变量可以在声明它的类和该类子类的其他实例方法中被访问。如果没有为实例变量指定保护级别,这是默认的变量范围。
- @public: 可以在任何位置访问实例变量。
- @package: 可在通过任何类实例和函数访问实例变量,但在包之外,实例变量会被当成私有变量。该范围可用于库和框架中的类。
这些指令称为访问修饰符,用于修饰在实例变量声明语句块中声明的实例变量。代码清单A-1展示了类实例变量的声明语句,这段代码声明了一个可在任何位置访问的变量(int型变量counter)、一个只能在声明它的类及该类的子类中访问的变量(float型变量temperature),以及一个只能在声明它的类中访问的变量(NSString *型变量descripton)。
代码清单A-1 使用访问修饰符声明实例变量
{
@public int counter;
@protected float temperature;
@private NSString *description;
}
A.2 数据类型
因为 Objective-C 是 C 语言的严格父集,所以它支持所有 ANSI C 数据类型和类型限定符。Objective-C 还定义了许多本语言专用的数据型(布尔型、类实例类型、id类型、类定义类型、块类型、枚举类型)。下面几节详细介绍这些内容。
A.2.1 布尔型
布尔型是用于存储布尔值的 Objective-C 数据类型,它有两个值:真值和假值。在 Objective-C 中,这两个值被定义为 YES 和 NO,而不是 true(1) 和 false(0)。为一个布尔值分配的存储大小为1字节。Objective-C 还支持 C99 类型的 _Bool 和 bool,但 BOOL 是 Objetive-C 中首选布尔数据类型。
A.2.2 类实例类型
类实例类型代表对象、类和超类(superclass)。Objective-C 运行时环境中有多种类实例类型,其中最常用的是 id 类型。
1. id类型
id 类型是所有 Objective-C 对象指针的根类型,用于存储对任何 Objective-C 对象(不论哪种类型)的引用。下面的语句声明了一个类型�为 id、名为 myObject 的变量。
id myObject; // 该变量的实例化对象可以是任何类型
Objective-C 的 id 类型在程序运行时等价于一种 C 语言结构,该结构被定义为指向类型定义结构 objc_object 的指针,如代码清单A-2所示。
代码清单A-2 id 类型的定义
typedef struct objc_object
{
Class isa;
} *id;
换言之,id 仅是一个指向含有 objc_object 标识符的 C 语言结构的指针。
2. instancetype 关键字
instancetype是一个上下文关键字,可用作方法结果的数据类型,它表明方法返回了相关结果类型。方法返回相关结果类型是指方法返回接收对象所属类的实例。通过相关结果类型可以推断出下列方法类型:
- 以 alloc 或 new 开头的类方法;
- 以 autorelease、init、retain 或 self 开头的实例方法。
下面语句使用关键字 new 创建了一个 OrderItem 对象。
OrderItem *item = [OrderItem new];
尽管 NSObject 类的 new 方法将 id 设置为返回类型,但编译器会根据名为 item 的接收对象所属的类,推断正确的结果类型(本例中为 OrderItem *)。
instancetype 关键字可用于明确指明,方法返回的是相关结果类型。代码清单A-3展示了 OrderItem 类的一个例子,该类的接口定义了一个返回相关结果类型的方法。
代码清单A-3 使用 instancetype 关键字返回相关结果类型的值
@interface OrderItem : NSObject
+ (instancetype)orderItemWithDescription:(NSString *)description
price:(float)cost;
@end
A.2.3 类定义类型
类定义类型定义了 Objective-C 运行时数据结构。它们包括 SEL、IMP、Class、objc_object类型。
1. SEL 类型
SEL 类型代表方法选择器,是一种在 Objective-C 运行时环境中代表方法名称的数据类型。使用编译器指令 @selector,可以为名为 helloworld 的方法选择器创建 SEL 类型的变量:
SEL myHelloMethod = @selector(helloWorld);
该选择器可用于调用对象中的 helloWorld 方法。
2. IMP 类型
IMP 类型代表一种方法指针,这种指针指向实现方法的函数的内存起始地址。下面是定义 IMP 的类型的语法:
typedeff id (*IMP)(id, SEL, ...);
如这个类型定义所示,被 IMP 类型指向的函数接收一个 id 类型、一个方法选择器(SEL)和可变参数(由可变参数符号...代表)作为参数,并返回 id 类型的结果。
3. Class 类型
Class 类型代表 Objective-C 类实例(即对象),由各种数据元素构成。下面是定义 Class 数据类型的语法:
typedef struct objc_class *Class;
如这个类型定义所示,Class 数据类型是一种指针,这种指针指向带 objc_class 标识符的不透明类型。objc_class 结构的成员只能由专门定义的函数(尤其是使用 Objective-C 运行时库函数)访问。
4. objc_object 类型
Objective-C 对象拥有相应的运行时数据类型。当编译器解析 Objective-C 对象的代码时,会生成创建运行时对象类型的代码,即 objc_object 类型。如代码清单A-4所示, 这种数据类型是一种带 objc_object标识符的 C 语言结构。
代码清单A-4 objc_object
struct objc_object
{
Class isa;
/* ...含有实例变量值的可变长度数据... */
}
objc_object类型含有一个变量,该变量的类型为 Class、名称为 isa;换言之,它是一个指向 objc_class 类型变量的指针。
A.2.4 块类型
块提供了创建一组语句(即语句块)并将其赋予变量(以便在将来调用)的方式。块类似于 C 语言的标准函数,但块除了是可执行代码外,还含有与堆内存或栈内存绑定的变量。块是一种 Objective-C 对象(类型为块),而且与所有 Objective-C 对象一样,它也是通过指针被访问的。像处理 Objective-C 对象一样,使用 id 类型可以引用块对象。下面的语句声明了一个名为 incrementSum 的块类型变量,该变量拥有一个类型为 int 的参数并且不会返回任何值。
void (^incrementSum)(int);
可以使用存储修饰符 __block 创建块变量。它可以应用于同一词法范围内、块常量外部的变量,并使用该变量能够在这个块常量中修改。该存储修饰符无法与本地存储修饰符 auto、register 和 static 应用于同一个变量。代码清单A-5给出了 __block 存储修饰符的用法。
代码清单A-5 __block 存储修饰符的用法
__block int totalSum = 0;
void (^incrementSum)(int) = ^(int amount) {
totalSum += amount;
NSLog(@"Current total = %d", totalSum);
};
incrementSum(5);
执行代码清单A-5所示代码,会输出如下结果;
Current total = 5
因此,块变量 totalSum 被初始化为0,然后在调用该块常量时被修改。
A.2.5 枚举类型
Objective-C 支持标准的 C 语言枚举类型,并且为指定枚举的类型提供了支持。枚举类型由使用关键 enum 定义的一系列已命名值(也称为“枚举器”)构成。被声明为枚举型的变量可以被赋予任何枚举器值。代码清单A-6定义了含有一组值的枚举型变量,该变量的名称为 color,含有 Red、Green 和 Blue 值。
代码清单A-6 枚举型定义示例
enum
{
Red,
Green,
Blue,
} color;
枚举器的默认类型为 int。Objective-C 支持显示地设置枚举的类型,这也称为“含有固定基础类型的枚举类型”。枚举支持的基础类型是带符号或不带符号的整型(char、short、int、long)。类型应该在 enum 关键字之后指定,而且前面要加冒号(:)。代码清单A-7所示的代码定义了一个枚举型变量,它的名称为 color,含有一个组类型为 unsigned char 的值:Red、Greed、Blue。
代码清单A-7 含有固定基础类型的枚举类型
enum : unsigned char
{
Red,
Green,
Blue,
} color;
通常,可使用类型定义声明枚举类型。代码清单A-8所示的代码使用类型定义声明了一个枚举型变量,该变量的名称为 color,含有一组类型为 unsigned char 的值:Red、Greed、Blue。
代码清单A-8 含有固定基础类型的枚举类型定义
typedef enum : unsigned char
{
Red,
Green,
Blue,
} ColorType;
ColorType color;
在 Objective-C 中,定义枚举型值的首选方式是用 NS_ENUM 宏。该宏提供一种约定机制来声明枚举值的基础类型,并使用一条语句定义新的枚举类型。代码清单A-9使用 NS_ENUM 宏定义了代码清单A-8中定义的枚举类型。
代码清单A-9 使用 NS_ENUM 宏定义枚举类型
typedef NS_ENUM(unsigned char, ColorType)
{
Red,
Green,
Blue,
};
如代码清单A-9所示,这个宏的第一个参数是基础类型,而它的第二个参数是新枚举类型的名称。像普通块语句一样,这些值会存储在语句块中。
A.3 操作符
Objective-C 支持 ANSI C 定义的操作符,尤其是下面这些操作符:
- 赋值操作符
- 算术操作符
- 复合赋值操作符
- 增量和减量操作符
- 逻辑比较操作符
- 逻辑判断操作符
- 逐位运算操作符
- 复合逐位运算操作符
- 三元运算操作符
- 类型转换操作符
- sizeof操作符
- 地址操作符(&)
- 间接操作符(*)
- 逗号操作符
要详细了解这些算术转换法则以及这些操作符的优先等级和关系,请参阅 ANSI C 的规定。
脱字操作符(^)用于处理块对象,特别是声明块对象类型和定义块常量。
A.4 语句
Objective-C 支持 ANSI C 定义的条件、循环和跳转语句,尤其是下面这些语句:
- 条件语句(if、if else、if else if、switch)
- 循环语句(for、while、do while)
- 跳转语句(return、goto、break、continue)
A.5 类元素
Objective-C 中的类由多个结构化元素构成,这些元素通常分为几个部分。本节概括介绍这些基本类元素。
A.5.1 实例变量
实例变量是为类声明的变量,用于在类实例的生命周期中保存实例的值。实例变量会在对象被初次创建时被分配内存,并在对象被释放时被释放内存。实例变量拥有与对象对应的默认作用范围和命名空间。可以在类接口、类实现文件或类扩展的实例变量声明语句块中声明实例变量。实例变量的声明语句块必须紧跟相应的接口、实现文件和类扩展声明语句。代码清单A-10展示了声明 Hello 类实例变量的例子。
代码清单A-10 Hello类实现文件中声明实例变量的语句块
@implementation Hello
{
// 在此处声明实例变量
...
}
...
@end
建议在类的实现文件(而不是公共的类接口)中声明实例变量。通常应在类实现文件专用的变量扩展中声明实例变量。
实例变量的所有权限定符
Objective-C 所有权限定符是一种只能应用于可保留对象指针的类型限定符。要设置变量的所有权规则,应在变量声明中将所有权限定符放置在对象指针类型之前。表A-2列出了4个所有权限定符。
限定符 | 描述 |
---|---|
__autoreleasing | 当变量被赋予新值时,新值会被保留和自动释放,而旧值会被发送释放消息 |
__weak | 变量使用简单赋值操作(即不复制或保留输入值)。如果对象被释放了,相应的变量值会被设置为nil |
__unsafe_unretained | 变量使用简单赋值操作(即不复制或保留输入值) |
__strong | 当变量被赋予新值时,新值会被保留,而旧值会被发送释放消息 |
默认的所有权限定符为__strong(在没有明确设置限定符的情况下)。下面语句声明了一个名为hello的变量(类型为 Hello *),其所有权限定符为 __weak。
__weak Hello *hello;
A.5.2 属性
在 Objective-C 中,属性通过访问方法(即获取方法和设置方法),为访问对象的共享状态提供了一种便利机制。属性与实例变量的区别在于,属性不直接访问对象的内部状态。在处理 Objective-C 属性时,编译器可以根据你提供的规格自动生成访问方法。
可以使用 @property 关键字声明属性,后跟一系列可选特性(封闭在圆括号内)、属性类型和属性名称。
@property (特性) 类型 属性名称;
属性声明特性用于设置存储语义和其他属性行为。表A-3列出了可用的属性特性。
表A-3 属性的特性
属性 | 描述 | 注释 |
---|---|---|
readwrite | 必选 | 访问限定符。默认设置 |
readonly | 必选 | 访问限定符。无法同时选择 readwrite 和 readonly |
assign | 使设置方法使用简单赋值操作(即不复制或保留输入值) | 所有权限定符。在手动内存管理方式(MRR)中使用。MRR 中的默认值 |
weak | 使设置方法使用简单赋值操作。如果属性实例被释放了,那么支持属性的实例变量会被设置为 nil | 所有权限定符。在 ARC 内存管理方式中使用 |
unsafe_unretained | 使设置方法使用简单赋值操作 | 所有权限定符。在 ARC 内存管理方式中使用 |
copy | 在使用设置方法赋值时,调用复制方法并向旧值发送释放信息 | 所有权限定符 |
retain | 在设置方法中,旧值会被发送释放消息,而属性会使用保留赋值操作 | 所有权限定符。与强引用特性类似(在手动内存管理方式中使用) |
strong | 在设置方法中,旧值会被释放消息,而属性会使用保留赋值操作 | 在 ARC 内存管理方式中使用的所有权限定符。ARC 中的默认设置 |
getter=getterName | 使用“获取方法的名称”指定获取方法的名称 | ---- |
setter=setterName | 使用“设置方法的名称”指定设置方法的名称 | ---- |
nonatomic | 将属性的访问方法设置为非自动生成(默认设置)。nonatomic 属性拥有更好的性能,但无法保证当多个线程访问属性时,能够通过属性访问方法返回具有线程安全性的值 | ---- |
属性定义是在类的实现文件中实现的。在大多数情况下,属性是由实例变量支持的,因此属性定义包括定义属性的获取方法和设置方法、声明实例变量以及在获取/设置方法中使用这些实例变量。 Objective-C 提供了3种定义属性的方式:显示定义、通过关键字补全和自动补全。
A.5.3 方法
Objective-C 既支持实例方法,也支持类方法。实例方法是指在类实例(对象)中调用的方法,因此,实例方法可以直接访问对象的实例变量。类方法是指在类中调用的方法。可以在类的接口、协议和类别(category)中声明方法。因此,应在相应的类/类别的实现文件中定义已声明的类。
方法的声明设置了方法的类型、返回值的类型、方法的名称、方法的参数和相应的参数类型。方法的类型标识符指明了该方法是类方法还是实例方法。类方法的声明中含有一个加号(+),它表明方法的作用范围为类,这意味着方法作用类一级,无法访问类中的实例变量(除非将它们作为参数传递给方法)。实例方法的声明中含有一个减号(-),它表明这些方法的作用范围为对象。这意味着这些方法作用于对象一级,而且可以直接访问对象及其父对象中的实例变量(在遵守这些实例变量访问控制规定的情况下)。
方法的名称中含有参数和冒号。每个参数的类型都是在圆括号中设置的,后跟方法原型参数。参数的名称和类型由冒号分隔,如下例所示:
-(void)incrementSum:(int)value;
该方法的名称为 incrementSum:,它拥有一个 int 型的参数 value。如果方法含有多个参数,就需要多个名称-参数对,如下例所示:
-(int)addAddend1:(int)a1 addend2:(int)a2;
该方法的名称为 addAddend1:addend2:,它拥有两个 int 型的参数 a1 和 a2。
返回类型用于设置方法返回变量的类型(如果存在返回变量)。返回类型是在圆括号中设置的,后跟方法的类型。如果方法没有返回值,那么就应将返回类型声明为 void。
下面的语句声明了一个类方法,它的名称为 hydrogenWithProthons:neutrons:,拥有两个 unsigned int 型的参数 nProtons 和 nNeutrons。
+ (void)hydrogenWithProthons:(unsigned)nProtons neutrons:(unsigned)nNeutrons;
A.5.4 接口
类接口设置了类的状态和行为,封装了实例变量、属性和方法。类接口的声明以 @interface 指令和类名称开头,以 @end 指令结束。代码清单A-11展示了声明类接口的正式语法。
代码清单A-11 类接口的声明语法
@interface 类的名称 : 父类的名称
{
// 实例变量的声明
...
}
// 属性和方法的声明
...
@end
父类创建了通用接口和实现代码,子类可以继承、修改和补充这些元素。Objective-C 中的绝大多数类都是 NSObject 类的子类,NSObject 类是大多数 Foundation 框架的根类(基类),它提供了 Objective-C 运行时环境的基础接口。
提前声明
使用 @class 指令可以在获取类的完整规格前,在类接口中声明类。这样做可以在无须导入相应头文件的情况下,告诉编译器类的存在,从而使编译器能够执行静态类型检查。提前声明可用于解决文件之间的循环依赖问题。
A.5.5 实现代码
类的实现文件通过实现类的属性和方法定义了类的行为。类实现文件以 @implementation 指令和类的名称开头,以 @end 指令结束。代码清单A-12展示了声明类实现文件的正式语法。
代码清单A-12 类实现文件的语法
@implementation 类的名称
{
// 实例变量的声明
...
}
// 属性和方法的定义
...
@end
在类实现文件中定义的方法必须直接与相应的类接口对应,例如,代码清单A-13所示的代码创建了 Greeting 类的接口和相应的实现代码。
代码清单A-13 Greeting类接口和实现代码
#import <Foundation/Foundation.h>
@interface Greeting : NSObject
@property (readwrite) NSString *salutation;
-(void)hello:(NSString *)user;
@end
#import "Greeting.h"
@implementation Greeting
-(void)hello:(NSString *)user
{
NSLog(@"%@, %@", self.salutation, user);
}
@end
A.5.6 协议
协议规定了类可以实现的方法和属性。协议的声明以 @protocol 指令开头,后跟协议的名称,以 @end 指令结束。协议中含有必选和可选方法。协议的实现代码无须实现可选方法。@required 指令和 @optional 指令(后跟一个或多个方法名称)用于设置必选和可选方法。如果没有明确设置这两个关键字中的任何一个,那么默认为使用 @required 指令。代码清单A-14展示了声明协议的语法。
代码清单A-14 声明协议的语法
@protocol 协议的名称
// 属性的声明
@required
// 方法的声明
@optional
// 方法的声明
@end
通过在尖括号中设置其他协议,可以组合使用协议,这称为遵守其他协议。逗号用于分隔多个协议,如代码清单A-15所示。
代码清单A-15 组合使用多个协议
@protocol 协议的名称<协议的名称>
// 方法的声明
@end
通过类似的语法可以使用接口遵守多个协议,如代码清单A-16所示。
代码清单A-16 使接口遵守协议的语法
@interface 类的名称 : 父类的名称<协议的名称>
// 方法的声明
@end
通常,类的接口和实现代码存储在不同的文件中,存储接口的头文件拥有 .h 后缀,而实现文件拥有 .m 后缀。
A.5.7 类别
通过类别(category)可在不进行子类化的情况下,为已经存在的类添加功能。类别中的方法会变成类的组成部分(在程序范围之内),并且会被该类的所有子类继承。
类别接口声明以 @interface 关键字开头,后跟已经存在的类名称、由圆括号封装的类别名称以及类别遵守的协议(如果存在),以 @end 关键字结束。应在这些语句之间添加方法的声明。代码清单A-17展示了声明类别的语法。
代码清单A-17 声明类别的语法
@interface 类的名称(类别的名称)
// 方法的声明
@end
与类的实现文件相似,类别在其实现中定义方法。代码清单A-18展示了类别实现的语法:
代码清单A-18 类别实现的语法
@implementation 类的名称(类别的名称)
// 方法的定义
@end
A.5.8 类扩展
可以将类扩展为匿名类别。在类扩展中声明的方法必须在相应类的实现文件中实现。与类别相反,类扩展可以声明实例变量和属性。代码清单A-19展示了类扩展的语法:
代码清单A-19 声明类扩展的语法
@interface 类的名称()
{
// 实例变量的声明
}
// 属性的声明
// 方法的声明
@end
通常,可以将类扩展放在类的实现文件中,使用它们组织和声明额外的、必要的私有方法(如不属于已声明的公用 API 的方法),以单独在某个类中使用。