本章着重介绍了用于开发类的关键元素和独有特性,其中包括 Objective-C 类的结构、类的设计与实现,以及其它一些支持类开发和OOP的语言特性。
<h3 id=”classdevelopment”>类的定义</h3>
在 Objective-C 语言中,类由接口和实现代码组成,一般分别放在2个文件中:类接口 .h 文件,类实现 .m文件。类接口声明了类的属性和方法;类实现定义了类的实例变量、属性和方法。
表3.1列出了常用的扩展名。
扩展名 | 含义 | 扩展名 | 含义 |
---|---|---|---|
.c | C 语言源文件 | .mm | Objective-C++ 源文件 |
.cc、.cpp | C++ 语言源文件 | .pl | Per 源文件 |
.h | 头文件 | .o | Object(编译后)文件 |
.m | Objective-C 源文件 | —— | —— |
类接口:类的声明使用关键字@interface和@end来声明。
// 代码清单-3.1
//
@interface ClassName : SuperClassName
//
// 属性和方法的声明
//
@end
类实现:类的实现使用关键字@implementation和@end来实现。
// 代码清单-3.2
//
@ implementation ClassName
//
// 实例变量(最好在类的实现部分声明实例变量)
// 在类接口中声明的所有方法都必须在类的实现文件中定义
//
@end
<h3 id=”classvariable”>实例变量</h3>
实例变量是指为类声明的变量,它们在相应的类实例(即对象)的生命周期中存在并拥有值。实例变量拥有与对象对应的作用范围和命名空间。当对象被创建时,系统会为实例变量分配内存,当对象被释放时系统也会释放变量占用的内存。
实例变量可以在类的接口或实现部分中声明,不过,在类的接口中声明实例变量会违反OOP的关键宗旨之一(封装),因此,最好在类的实现部分中声明实例变量,而当语句块紧跟类的 @implementation 指令时,尤其如此。
// 代码清单-3.3
//
@ implementation ClassName
{
// 声明实例变量的代码(最好在类的实现部分声明实例变量)
@private NSString *description;
@protected float temperature;
@public int counter;
...
}
...
@end
Objective-C 定义了多条编译指令,使用这些指令可以控制实例变量的范围,即在程序中控制变量的可见性。
- @private: 实例变量只能在声明它的类和该类的其他实例中被访问。
- @protected: 实例变量可以在声明它的类和该类子类的其他实例方法中被访问。如果没有为实例变量指定保护级别,这是默认的变量范围。
- @public: 可以在任何位置访问实例变量。
- @package: 可在通过任何类实例和函数访问实例变量,但在包之外,实例变量会被当成私有变量。该范围可用于库和框架中的类。
这些指令称为访问修饰符,用于修饰在实例变量声明语句块中声明的实例变量。
<h3 id=”classproperty”>属性</h3>
Objective-C 中属性与实例变量的区别是:实例变量存储了对象的内部状态,可以直接获取对象内部状态。而属性无法直接访问对象的内部状态,但提供了访问这类数据的方便机制(即读取和设置方法),因而可以含有其它逻辑。
大多数属性都是由实例变量支持的,属性通过这种机制隐藏对象的内部状态。除非专门设置,否则实例变量就拥有与属性相同的名称(但会带下划线前缀)。
属性的声明:使用关键字 @property 后跟一组可选的特性(用圆括号括起来)、属性的类型和名称。
// 代码清单-3.4
//
@property(特性)属性的类型 属性的名称;
// 属性的特性
//
// 原子性特性 nonatomic 使用该特性可以在多线程并发的情况中,将访问器设置为非原
// 子性的,因而能够提供不同的结果。如果不设置该特性,访问
// 器就会拥有原子性,换言之,赋值值和返回结果永远都会完全同步
// 设置器语义 assign 通过该特性可以在不使用copy和retain特性的情况下,使属
// 性的设置器方法执行简单的赋值操作。这个特性是默认设置。
// 设置器语义 retain 在赋值时,输入值会被发送一条消息,而上一个值会被发送一条
// 释放信息
// 设置器语义 copy 在赋值时,输入值会被发送一条消息的副本,而上一个值会被
// 发送一条释放信息
// 设置器语义 strong 当属性使用ARC内存管理功能时,该特性等同于retain特性
// 设置器语义 weak 当属性使用ARC内存管理功能时,该特性的作用与assign特性类似,
// 但如果引用对象被释放了,属性的值会被设置为nil
// 可读写特性 readwrite 使用该特性时,属性可以被读取也可以被写入,而且必须实
// 现getter和setter方法。这个特性是默认设置
// 可读写特性 readonly 使用该特性时,会将属性设置为只读。必须实现getter方法
// 方法名称性 getter=getterName 将getter方法重命名为新设置器的名称
// 方法名称性 setter=setterName 将setter方法重命名为新设置器的名称
其中特性有如下几种取值,各个特性的含义涉及到 Objective-C 中内存管理的相关知识,后面会有详细的讲解,所以这里只是简单的介绍。只有对 Objective-C 的内存管理有了比较全面的了解之后,才能很好的理解这里各个特生的含义。
◉ 读写属性:(readwrite/readonly)决定是否生成set访问器
◉ setter语意:(assign/retain/copy)set访问器的语义,决定以何种方式对数据成员赋予新值
◉ 原子性:(atomic/nonatomic)
属性特性详细讲解:
readwrite:生成 setter\getter 方法(默认)
使用该特性时,属性可以被读取也可以被写入,而且必须实现getter和setter方法。这个特性是默认设置。
readonly:只生成 getter 方法.
此标记说明属性是只读的,如果你指定了 readonly,在 @implementation 中只需要一个 getter。或者如果你使用@synthesize关键字,也只会生成getter方法。如果你试图使用点操作符为属性赋值,你将得到一个编译错误。readonly关键字代表 setter 不会被生成, 所以它不可以和 copy/retain/assign 组合使用。
assign:简单赋值,不更改索引计数
此标记说明设置器直接进行赋值,这也是默认值。在使用垃圾收集的应用程序中,如果你要一个属性使用 assign,且这个类符合 NSCopying 协议,你就要明确指出这个标记,而不是简单地使用默认值,否则的话,你将得到一个编译警告。这再次向编译器说明你确实需要赋值,即使它是可拷贝的。
retain:释放旧的对象,将旧对象的值赋予输入对象,再增加输入对象的索引计数为1
指定 retain 会在赋值时唤醒传入值的retain 消息。此属性只能用于Objective-C 对象类型,而不能用于Core Foundation 对象。(原因很明显,retain 会增加对象的引用计数,而基本数据类型或者 Core Foundation 对象都没有引用计数)。
copy:建立一个索引计数为1的对象,然后释放旧对象
它指出,在赋值时使用传入值的一份拷贝。拷贝工作由 copy 方法执行,此属性只对那些实行了 NSCopying 协议的对象类型有效。更深入的讨论,请参考”复制”部分。
atomic/nonatomic:原子操作
指出访问器不是原子操作,atomic 表示属性是原子的,支持多线程并发访问,而默认地 nonatomic,访问器是原子操作。这也就是说,在多线程环境下,解析的访问器提供一个对属性的安全访问,从获取器得到的返回值或者通过设置器设置的值可以一次完成,即便是别的线程也正在对其进行访问。如果你不指定 nonatomic,在自己管理内存的环境中,解析的访问器保留并自动释放返回的值,如果指定了 nonatomic,那么访问器只是简单地返回这个值。没有特别的多线程要求建议用 nonatomic 有助于提高性能。
在 iOS5 引入了自动引用计算 ARC(Automatic Reference Counting)之后,对象变量属性新增了 strong 和 weak,strong 与 retain 作用类似,可以说是用来代替retain;weak 与 assign 特性类似.
<h3 id=”classgetproperty”>访问属性</h3>
属性的定义:大多数属性是由实例变量支持的,因此属性的定义中会含有属性的 getter 和 setter 方法的定义、实例变量的声明,并在 getter/setter 方法中使用这些变量。Objective-C 提供了多种定义属性的方式:显示定义、通过关键字(@synthesize)补全、自动补全和动态生成(@danamic)。
Objective-C 提供了两种访问属性的机制:访问器方法和点语法。
访问器方法:用于读取值的方法(getter方法)拥有与属性相同的名字;用于设置值的方法(setter方法)其名称以set开头、后跟首字母大写的属性名。
// 代码清单-3.5
//
// 假设属性名称为 color
[myObject color];
[myObject setColor:输入值];
点语法:类似于Java语言中的方法.
// 代码清单-3.6
//
// 假设属性名称为 color
myObject.color;
myObject.color = 输入值;
Objective-C 语法关于点表达式的说明:如果点表达式出现在等号 = 左边,该属性名称的 setter 方法将被调用。如果点表达式出现在 = 右边,那么该属性名称的 getter 方法将被调用。所以在 Objective-C 中点表达式其实就是调用对象的 setter/getter 方法的一种快捷方式。
<h3 id=”classvariableproperty”>实例变量与属性的关系</h3>
在老版本的 Objective-C 语言中,我们需要同时声明属性和实例变量,那时属性是 Objective-C 语言的一个新的机制,并且要求你必须声明与之对应的实例变量,例如:
// 代码清单-3.7
//
@interface MyNoteBook : NSObject
{
@protected NSString * _content;
}
@property (nonatomic, retain) NSString * content;
...
@end
后来,苹果将默认编译器从GCC转换为Clang/LLVM(low level virtual machine),从此不再需要手动为属性声明实例变量了,它支持对已声明属性进行自动补全。如果编译器发现一个没有实例变量支持的属性,它将自动创建一个作用范围为 @priavte 并且与属性名称相同的支持实例变量(但会带下划线前缀)。编译器可以自动补全以下声明的属性:1、没有使用关键字(如@synthesize)进行补全的属性;2、不是(通过@dynamic属性指令)动态生成的属性。3、没有用户编写的 getter 和 setter 方法的属性。因此,使用该特性就无需手动补全已声明的属性。编译器会自行补全已声明的属性和相应的实例变量。
例如 MyNoteBook.h 文件:
// 代码清单-3.8
//
@interface MyNoteBook : NSObject
@property (nonatomic, retain) NSString * content;
...
@end
在 MyNoteBook.m 文件中,编译器也会自动的生成一个实例变量 _content ,那么在 .m 文件中可以直接的使用 _content 实例变量,也可以通过属性 self.content 来访问和设置。
注意:这里的 self.content 其实是调用属性 content 的 getter/setter 方法。这与 C++ 中点的使用是有区别的,C++中的点可以直接访问成员变量(也就是实例变量)。
例如 MyNoteBook.h 文件:
// 代码清单-3.9
//
@interface MyNoteBook : NSObject
{
__strong NSString * content;
}
...
@end
在 MyNoteBook.m 文件中,self.content 这样的表达式是错误的。Xcode会提示你使用 ->,改成 self->content 就可以了。因为 Objective-C 中点表达式是表示调用方法,而上面的代码中没有 content 这个方法。
以前的用法,声明属性跟与之对应的实例变量(代码清单-3.7),这种方法基本上使用最多,现在大部分也是在使用,因为很多开源的代码都是这种方式。但是在 iOS5 更新之后,苹果是建议用以下的方式来使用:
// 代码清单-3.10
//
@interface MyNoteBook : NSObject
@property (nonatomic, retain) NSString * title;
@property (nonatomic, retain) NSString * author;
@property (nonatomic, retain) NSString * content;
...
@end
因为编译器会自动为你生成与属性名称相同(但带下划线前缀)的实例变量;也会自动为你生成 setter/getter 方法。
如果你想自己设定与属性相关联的变量的名称,则可以通过关键字 @synthesize 来实现。
// 代码清单-3.11
//
// 语法:@synthesize 属性名称 [=实例变量名称];
// 例如:
@synthesize myIntProperty; // 省略[=实例变量名称]
@synthesize myIntProperty = myPropertyOne;
如果省略了可选项[=实例变量名称],编译器会根据属性实例变量标准命名惯例,自动生成实例变量的名称。如果你设置了可选项[=实例变量名称],编译器就会使用该名称创建实例变量。
<h3 id=”classmethod”>方法</h3>
方法的声明由方法类型、返回值类型和一个或多个(提供名称、参数和参数类型信息的)方法代码段构成。
// 代码清单-3.12
//
// 语法:方法类型 (返回类型) 方法代码段名称 : (参数类型) 参数名称 ...
// 例如:
- (id) initWithTitle: (NSString *) title;
- (id) initWithTitle: (NSString *) title andContent: (NSString *) content;
+ (void) readMyNotBook: (NSString *) title;
调用方法:对象(发送器)通过发送信息与其它对象(接收器)进行交互,从而调用指定的方法。
// 代码清单-3.13
//
// 语法:[接收器 方法代码段名称:参数值 ...]
// 说明:如果拥有多个代码段,可将它们的名称和参数值以空格为分隔符连续排列。
// 例如:
[myNoteBook initWithTitle:@"Title"];
[myNoteBook initWithTitle:@"Title" andContent:@"BookContent"];
<h3 id=”classprotocol”>协议</h3>
使用协议声明的方法和属性可以由任何类实现。协议不与特定的类关联,因此,使用它可以捕捉无继承关系的类之间的相似之处。协议使 Objective-C 支持多重继承规范的概念(方法声明)。
// 代码清单-3.14
//
// 语法:
@protocol 协议名称
// 属性声明
@required
// 方法的声明(必选方法)
@optional
// 方法的声明(可选方法)
@end
通过在尖括号中设置已经声明的协议的名称,可以使一个协议与其它协议合并——这称为接受协议。可使用逗号分隔多个协议。如代码清单-3.15所示
// 代码清单-3.15 合并其它协议
//
// 语法:
// 合并单个协议
@protocol 协议名称 <协议名称>
// 方法的声明
@end
// 合并多个协议
@protocol 协议名称 <协议名称1, 协议名称2, 协议名称3, ... >
// 方法的声明
@end
使用类似的语法可以令接口接受其它协议,如代码清单-3.16所示
// 代码清单-3.16 接口接受其它协议
//
// 语法:
// 接口接受单个协议
@interface 类的名称 :父类的名称 <协议名称>
// 方法的声明
@end
// 接口接受多个协议
@interface 类的名称 :父类的名称 <协议名称1, 协议名称2, 协议名称3, ... >
// 方法的声明
@end
协议不引用任何类,它是类无关的,任何类都可以实现定义好的 Protocol。如果我们想知道某个类是否实现了某个 Protocol,可以使用 conformsToProtocol 进行判断,如下:
if(YES == [obj conformsToProtocol:@protocol(ProtocolName)]) {
// 在这里插入代码
}
这里使用 @protocol 指令用于获取一个协议名称,并产生一个 Protocol 对象,并作为conformsToProtocol: 的参数。如果为了测试 obj 是否实现了协议中的某一个方法,可以编写以下代码:
if ([obj respondsToSelector:@selector(methodName)]) {
[obj methodName];
}
<h3 id=”classcategory”>分类</h3>
使用分类可以在不进行子类化的情况下,为已经存在类增加功能。分类通常用于:1、扩展其他人定义的类(即使你无法访问它们的源代码);2、替代子类;3、将新类的实现代码分发给多个源文件(通过多人分工,简化大型类的开发工作)。
分类接口的声明以关键字 @interface 开头,后跟已存在的类的名称、带括号的分类名称以及它所接受的协议(如果有的话),以关键字 @end 结束。方法的声明放在这些语句之间。
// 代码清单-3.17 分类声明的语法
//
// 语法:
@interface 类的名称 (分类的名称) <协议名称1, 协议名称2, 协议名称3, ... >
// 方法的声明
@end
扩展视为一种匿名(即未命名的)分类。在扩展中声明的方法必须在相应类的主 @implementation 块中实现(它们无法在分类中实现)。
// 代码清单-3.18
//
// 语法:
@interface 类的名称 () <协议名称>
{
// 实例变量的声明
}
// 属性的声明
// 方法的声明
@end
扩展与分类的区别是它能声明实例变量和属性。编译器会检查在扩展中声明的方法(和属性)是否被实现。类扩展通常应存储在类实现文件中,并用于组织和声明在类中独立使用的其它私有方法(例如,不是公用API的一部分)。
<h3 id=”classsummary”>小结</h3>
本章主要介绍了 Objective-C 程序开发中使用的类。
- Objective-C 的类由接口和实现代码构成。接口声明了类的属性和方法;实现定义了类的实例变量、属性和方法。按照惯例,应将类的接口代码存储在 .h 文件中,类的实现代码存储在 .m 文件中。
- 在协议中声明的方法和属性可以由任何类实现。使用协议通常可以在无继承关系的类之间实现通用行为。
- 使用分类可以在不进行了类化的情况下,为已经存在的类增加功能。分类通常用于:1、扩展其他人定义的类(即使你无法访问它们的源代码);2、替代子类;3、将新类的实现代码分发给多个源文件。扩展可以被视为一种匿名分类,不过它声明的方法必须在类的主实现块中实现。扩展还可以声明实例变量和属性。