定义一个类
大多数面向对象的程序代码都是为创建新的对象而设计——定义新的类。在Objective-C中,类是由两个部分构成的。
- 接口,声明了类的方法和实体变量以及父类的名称。
- 实现,真正定义类的地方(包含类的方法实现代码)。
他们被分配到两个文件中。但是有时候,类的定义可以被分配到多个文件中,利用一种叫“分类(category)”的特性。“分类”可以划分一个类的定义也可以扩展一个类。分类在“分类和扩展”章节中有描述。
源文件
类的接口和实现通常是分别保存到两个不同的文件中,尽管编译器没有这样要求。接口文件一定要对任何想使用这个类的用户有效。
也可以只用一个文件声明或实现多个类。但是,按照惯例,你应该让每一个类有单独的接口文件,即使类的实现还没有从接口文件分开。保持类的接口分离,更好的体现了类作为独立实体的状态。
接口文件和实现文件通常都以类的名字命名。实现文件用.m做文件的扩张名称,表明它包含Objective-C的源码。接口文件可以指定任何的扩展名。因为它经常被其它的源文件包含,因此接口文件通常都是用表示头文件的.h作扩展名称。比如,Rectangle类会在Rectangle.h文件中做声明,在Rectangle.m文件中做定义。
将对象的接口与实现分离,很适合面向对象设计。对象就是一个功能完备的实体,从外面它几乎就是一个“黑盒”。一旦你确定怎么让一个对象与你程序中其它元素进行交互——也就是你已经声明了接口——你就可以随意的更改类的实现部分而不会对程序照成任何影响。
类接口
类接口的声明开始于编译指令@interface结束于编译指令@end。(所有的Objective-C的编译指令都是以@开头的)
@interface ClassName : ItsSuperclass
{
instance variable declarations
}
method declarations
@end
第一行的声明主要展现了新类的名称随后是它要继承的父类名称。父类决定新类在继承层次结构中的位置。关于继承在“继承”章节中已经讨论。如果冒号和父类名字忽略,你将定义的是根类,就像NSObject。
接下来用大括号括起来的是声明的实体变量,是每个类实例的数据结构部分。下面是声明在Rectangle类中部分实体变量。
float width;
float height;
BOOL filled;
NSColor *fillColor;
在实体变量声明结束和类定义结束之间用来声明方法。在方法之前用加号(+)标识的方法是供类对象使用的叫做“类方法”。
+alloc;
在方法之前用减号(-)标识的方法是供类的实例使用的叫做“实例方法”。
- (void)display;
你可以将类方法和实例方法使用相同的名子,尽管通常不这样做。也可以让实例方法的名称与实体变量的名称相同。这样做非常普遍,尤其是方法返回某个实体变量值。比如,Circle有个radius方法它就是和Circle的实体变量radius相对应的。
用C语言类型转换运算符声明返回类型。
- (float)radius;
参数类型也是用同样的方法声明的。
- (void)setRadius:(float)aRadius;
如果返回类型或参数类型没有明确的声明,系统就认为是使用的默认类型——id类型。我们曾经讨论过的alloc方法返回的就是id类型。
当方法中有多个参数时,参数声明在方法名称中冒号后面。在声明中参数的名称要用空格隔开,就像在一个消息里,举个例子:
- (void)setWidth:(float)widthheight:(float)height;
带有可变参数方法的参数声明用逗号将参数名称和省略号(…)分开,就像函数那样:
- makeGroup:group, ...;
引用接口
任何资源模块要想用类接口就要将接口文件包含其中——这其中包括创建类的实例、调用类方法发送消息、声明为类的实例变量。接口被包含用“#import”语法: #import "Rectangle.h"
它基本等同于“#include”,除了“#include”要保证不能包含同名文件。它因此作为首选使用,在Objective-C文档代码中使用“#include”的地方都由“#import”代替。
为了说明类的定义是建立在继承类的基础上的,在接口文件的开始处引入父类的接口文件。
#import"ItsSuperclass.h"
@interfaceClassName : ItsSuperclass
{
instance variabledeclarations
}
methoddeclarations
@end
这种约束意味着每一个接口文件都直接或间接的引入了其上层的整个继承层次结构中的类接口文件。
注意,如果支持预编译——一个预编译好的头文件——那么父类也支持预编译,你应该引用预编译好的。
引用其它的类
一个接口文件声明了一个类,通过引用它的父类,也暗含着对声明了对所有的继承的类的引用,也就是从NSObject向下一直到它的父类。如果接口提到的类不是继承体系中的类,那么就要明确的引入或者用@class语法声明它们: @class Rectangle, Circle;
这个指令只是简单的提醒编译器“Rectangle”和“Circle”都是类名。并不需要引入它们的接口文件。
在接口中类名提及主要用在实体变量的静态类型定义、返回值的类型声明和参数类型声明。比如这样一个声明
-(void)setPrimaryColor:(NSColor *)aColor;
中就提到了NSColor类。
像这种只是简单的将类名作为类型的声明,并不会依赖类接口的任何细节(它的方法和实体变量)。@class指令给编译器只是一个充分的预告,它需要这个类。如果接口类真正地被使用(创建实例,消息发送),类接口文件一定要引入。如果一个接口文件使用了@class去声明一个类,那么相应的实现文件就要引入声明的类接口文件(因为他们要实例那些类,会向它们发送消息)。
@class指令让编译器和连接器用最少的代码来理解声明的内容,也是对类名称声明最简单方法。正是由于这种简单,避免了两个文件因为互相引入,而要彼此等待的潜在问题。比如,一个类静态声明类型为另一个类的实体变量,并且两个类接口文件是互相引用的,这样两个类就无法继续编译。
接口的角色
接口文件的意图就是声明新的类给其它模块使用(也可以让其他的程序员使用)。它包含了与这个类工作的所有信息(程序员也可以通过文档获得)
- 接口文件会告诉用户类是怎样加到继承体系中的,以及定义中涉及到的类——继承的或简单引用的
- 接口文件会让编译器知道一个对象包含哪些实体变量,并且会告诉程序员哪些变量是从父类继承来的。尽管实体变量看上去理所当然在类的实现中声明而不是在接口中,但是无论如何它们都必须定义在接口文件中。这是因为一个对象在使用时,编译器必须清楚地知道它的结构,而不仅仅它是在哪里定义的。大多数程序员在使用类时会忽略类的实体变量,除非他要继承这个类。
- 最后,通过声明的方法列表,让其它的模块知道哪些消息可以发送给类对象,哪些是发送给类实例的。任何一个可以外部使用的方法都是定义在接口文件中的;在类实现文件中定义的方法可以忽略不计。
类的实现
类定义的结构和类声明的结构非常相似,它的开始使用@implementation指令,结束使用的@end指令:
@implementation ClassName : ItsSuperclass
{
instance variable declarations
}
method definitions
@end
每一个实现文件都要引入它的接口文件。比如,Rectangle.m文件就要引入Rectangle.h文件。因为实现不需要重复的声明它所引用的接口中的声明,完全可以放心的忽略:
- 父类的名字
- 声明的实体变量
这样就简化了实现,使实现更加集中在方法的定义上。
#import "ClassName.h"
@implementationClassName
methoddefinitions
@end
类方法的定义很类似C语言函数的定义,带有一对大括号,在括号的前面的定义与接口文件中声明一致,但是没有结束分号。举例如下:
+ (id)alloc
{
...
}
- (BOOL)isfilled
{
...
}
- (void)setFilled:(BOOL)flag
{
...
}
带有可变参数的方法对参数的操作就像函数那样:
#import <stdarg.h>
...
- getGroup:group, ...
{
va_list ap;
va_start(ap, group);
...
}
引用实体变量
实例方法在默认情况下,在其定义范围你可以引用任何实体变量。实体变量的引用通过变量的名字。尽管编译器会创建一个相当于C语言的结构体存储实体变量,但确切的结构体是隐藏的。你根本不需要使用结构体的操作(.或->)去引用一个对象的数据。下面方法的定义就引用接收对象的filled实体变量:
- (void)setFilled:(BOOL)flag
{
filled = flag;
...
}
接收对象和实体变量filled都没有作为参数传入方法中,但是filled却可以放在它的定义范围中。这种简化的方法语法在Objective-C代码中就是一个非常典型的缩略。
当实体变量属于一个对象但不是接收对象,那么这个对象要通过静态类型定义让编译器清楚它的类型。在引用一个静态类型对象的实体变量时,使用结构体指针操作符(->)。
假设Sibling类静态类型声明一个对象twin作为实体变量:
@interface Sibling : NSObject
{
Sibling *twin;
int gender;
struct features *appearance;
}
只要静态类型声明的对象的实体变量在类的作用范围之内(在这里Sibling的类型和类相同),Sibling方法就可以直接设置实体变量:
- makeIdenticalTwin
{
if ( !twin ) {
twin = [[Sibling alloc] init];
twin->gender = gender;
twin->appearance = appearance;
}
return twin;
}
实体变量使用范围
尽管实体变量定义在接口中,但是它主要是用在类的实现中而不是直接用在类的外部。一个对象的接口依赖于它的方法而不是它内部的数据结果。
经常会在方法和实体变量有一对一的应用,如下代码:
- (BOOL)isFilled
{
return filled;
}
但这不能当做惯例,有些方法返回的信息并不会存储在实体变量中,有些实体变量的信息并不想让对象暴漏。
在类一次又一次的修改中,实体变量很可以也被修改,然而声明的方法可以保持不变。只要把消息作为与类实体交互的工具,这些改变根本就不会影响到它的接口。
为了迫使对象有隐藏数据的能力,编译器限定了实体变量的使用的范围——也就是限制它在程序中的可见性。为了达到灵活性,你可以明确的按三种级别设定实体变量的使用范围。每一种级别都有编译器指令标注。
指令 | 含义 |
@private | 实体变量只有声明它的类可以使用 |
@protected | 实体变量只能在类中及该类的子类中使用。 |
@public | 实体变量任何地方都可以使用 |
@package | 在64位系统中,实体变量在一个工程或框架的实现内部相当于@public在工程或框架外部看它是@private的。 这个非常类似于提供给变量和函数的private_extern 指令。任何从在工程或框架的类实现部分之外访问都会报出链接错误。这对框架类来说尤其有用,在框架实现中如果将变量设置为@private又过于严谨,用@public或@protected又过于宽泛。 |
这个在Figure 2-1中有所描述
指令会提供给所有列在指令后的实体变量,到下一个指令或者所有实体变量声明结束。在接下来的例子中,age和evaluation是私有的,name,job和wage是保护的,boss是共有的。
@interface Worker : NSObject
{
char *name;
@private
int age;
char *evaluation;
@protected
id job;
float wage;
@public
id boss;
}
默认,没有标注的实体变量(像name)都是@protected。
所有的实体变量无论怎么标注,在声明它的类内都是有效的,就像上面提到的Worker类,在它定义的方法中可以引用任何的实体变量:
- promoteTo:newPosition
{
id old = job;
job = newPosition;
return old;
}
很明显,如果一个类自己不能访问它自己的实体变量,那么这个实体变量根本毫无用处。
通常一个子类还可以访问它继承来的实体变量。引用实体变量的能力通常也会沿着变量被继承。让一个类在其范围之内拥有整个数据结构是非常合理的,特别当你认为一个类的定义是对其继承的类详细的描述。上面提到的promoteTo:方法就好像在所有类中都有定义,这些类从Worker类中继承了job实体变量。
然而,有几个原因你应该尽量不要直接访问继承类的实体变量。
- 一旦一个类访问了继承来的实体变量,那么这个声明的实体变量就和这个类的实现连续在一起。在以后的版本中,你就不能删除这个变量或改变它的角色,防止无意中破坏了继承了它的类。
此外,如果一个类继承了实体变量并且更改了它的值,这样就无意中给声明实体变量的类引入了bug,尤其这个实体变量在类的内部被涉及到依赖。
要把一个实体变量的作用域限制在定义它的类的范围之内,你就必须将它标注为@private。如果子类要访问父类被标注为@private的实体变量只能调用公共的访问方法,否则是无法访问的。
另一个极端就是将变量标注为@public使其成为一个通用的变量,甚至在类定义的外部。通常为了获取存储在实体变量中的信息,要向对象发送消息请求,然而访问一个公共的实体变量就像访问C语言的结构体中变量,举例:
Worker *ceo =[[Worker alloc] init];
ceo->boss =nil;
注意对象一定是静态类型的。
将实体变量标注为@public,破坏了对象对数据的隐藏。它违背了面向对象的一个基本原则——对象的数据封装性。数据封装可以避免查看和无意中的错误。公共实体变量应尽量避免使用,除非在极特殊的情况下。
向self和super发送消息
Objective-C提供了两个语法用来在方法定义中引用对象来执行方法——self和super。假设你定义一个可以改变任何对象的坐标位置的reposition方法。这个方法可以通过调用setOrigin::方法来实现这个功能。resposition方法所要做的就是给自己发送消息的对象发送setOrigin::消息。当你写代码的时候,你可以将self作为接收对象,也可以将super作为接收对象,因此resposition方法将有两种写法:
- reposition
{
...
[self setOrigin:someX :someY];
...
}
或者:
- reposition
{
...
[super setOrigin:someX :someY];
...
}
在这里,无论对象怎么样,只要对象接收了reposition消息就可以引用self或者super。但是这两个语法是完全不同的,self会被消息当成隐藏的参数发给每个方法;它在方法的定义中被当做局部变量任意使用,就像使用实体变量的名字一样。只有在self作为消息的接收对象时,super可以替代self。作为接收对象,它们严格的区别就是它们是如何影响消息的进程:
-
self寻找实现方法通常都是从接收对象类的调度列表中开始。在上面的例子中,会从接收reposition消息对象的类开始。
- super对实现方法寻找的流程是完全不同的,它是从接收消息对象的父类开始。在上面的例子中,会从定义reposition方法的类的父类开始寻找。
-
无论super在哪里接收消息,编译器都会给objc_msgSend函数替换新的消息发送流程,就好像直接从定义的类的父类开始——也就是类的父类发送消息给super——而不是接收消息对象的类。
举个例子
在三个类的继承结构中就可以清楚的看出self和super的不同。假设我们创建一个叫Low类的对象。Low的父类叫做Mid;Mid的父类叫做High。这三个类都定义了一个叫negotiate的方法,每个类对此方法都有不同的用途。另外,Mid还定义了一个调用negotiate方法的方法,叫做keLastingPeace。
如Figure 2-2中的插图:
现在给我们定义的Low对象发送消息执行makeLastingPeace方法,同时makeLastingPeace方法中又发送negotiate消息给我们的Low对象。如果代码中调用这个对象的self,
- makeLastingPeace
{
[self negotiate];
...
}
消息发送流程会找到定义在Low的中negotiate,也就是self的类中。但是,如果Mid代码中调用这个对象的super,
- makeLastingPeace
{
[super negotiate];
...
}
消息发送流程会在High类中找到negotiate。由于makeLastingPeace定义在Mid中,因此消息发送流程忽略接收对象的类(Low),掠过Mid类,直接跳到Mid的父类。
就像上面的代码那样,super提供了一种方法绕过覆盖方法调用被覆盖的方法。就像makeLastingPeace方法绕过Mid中negotiate方法,调用High中的negotiate方法。
不能触及到Mid的negotiate方法,看来似乎有点问题,但是在如下情况,避开使用是正确的:
-
Low类的作者故意覆盖Mid类中的negotiate,这样Low类实例(和Low类的子类)将会调用重定义的方法。也就是Low类的设计者不想让Low类的对象执行继承来的方法。
发送消息给super,实现Mid的makeLastingPeace方法的作者有意略过Mid的negotiate方法(也会略过任何想Low类,定义在Mid子类的方法)去执行Hight中的方法。Mid的设计者就想用High中的negotiate方法,而不是其它类的。
Mid中的negotiate也可以使用,只是你直接发送消息给Mid的实例就好了。
使用super
发送消息给super可以使方法的实现分布到多个类中。为了对一个继承来的方法修改或加新功能,你可以复写它,你甚至可以在复写的方法中引入原始的方法:
- negotiate
{
...
return [super negotiate];
}
对于一些时候,在继承层次结构中的类方法实现的工作一部分由自己完成,剩下的发送消息给super,由父类的方法完成。Init方法——初始化新分配的对象——就是这样设计的。每个init方法都有责任去初始化其类中的实体变量,但是在这样做之前,它要发送一个init消息给super初始化父类的实体变量。每一个版本的init都沿袭这样的流程,最终整个继承中的类的实体变量都会初始化:
- (id)init
{
if (self = [super init]) {
...
}
}
初始化方法还有些附加的规则,这方面的讨论将在“分配和初始化对象”中讨论。
也可能是将一些核心的功能集中放在父类中,子类通过给super发送消息将父类中的方法包含起来。举例来说,每个类都有创建实例的方法,用来给新创建的实例分配内存初始化isa指针指向类结构体。典型的做法就是将alloc和allocWithZone:定义在NSObject类中,即使别的类复写了这些方法(这很少见),它仍然可以向super发送消息,继续使用NSObject中的初始化功能。
重新定义self
super是一个简单的标识,告诉编译器它在哪开始寻找方法去执行。它只为用作消息的接收对象。但是self是一个变量名字,它有多种使用方法,甚至可以给它赋新值。
也有将self用在类方法定义中的。类方法关心的不是类对象而是类的实例。比如说,有些类方法具有分配新对象和给新对象初始化功能,以此同时还会给一些实体变量赋值。在这样的方法中,很有可能试图发送消息给刚刚分配的实例,然后调用实体self。这么做在实例方法是可以的,但是在类方法中是错误的。self和super都被看作接收对象——收到消息的对象执行方法。在实例方法中,self指的是实例;但是在类方法中self指的是类对象。下面的例子是错误的:
+ (Rectangle *)rectangleOfColor:(NSColor *) color
{
self = [[Rectangle alloc] init]; // BAD
[self setColor:color];
return [self autorelease];
}
为了避免混淆使用,在类方法中尽量将实例赋给变量而不是给self。
+ (id)rectangleOfColor:(NSColor *)color
{
id newInstance = [[Rectangle alloc] init]; // GOOD
[newInstance setColor:color];
return [newInstance autorelease];
}
实际上,发送alloc消息最好在类方法中,并且最好将alloc发送给self。这样,如果类是子类,并且rectangleOfColor:消息发送给了子类,那么返回的实例类型也和子类的类型相同(比如说,NSArray的array方法,就被NSMutableArray类继承)。
+(id)rectangleOfColor:(NSColor *)color
{
id newInstance =[[self alloc] init]; // EXCELLENT
[newInstancesetColor:color];
return[newInstance autorelease];
}