Objective-C 编程语言官网文档(三)-如何定义类
转自http://blog.csdn.net/micheal_j/article/details/7616664
声明:本文档仅为个人学习过程中顺手翻译之作,方便开发的同胞借鉴参考。如有觉得译的不好不到位的地方,欢迎指正,将及时做出更正
尽量尊重原文档,因为首次Objective-C,有些地方可能直译了没有注意该语言的专有词,希望指正。如需转载,请注明出处
如何定义类
大多数面对对象编程都包含为新对象编写代码---定义一个新类。在Objective-C中,类被定义为两部分:
-
一个接口 声明有一些方法以及类的属性,以及它的父类的名字
-
一个实现类 真正的类 (包括实现的方法的代码)
下面的几部分通常在它自己的文件里。然而,有是有,一个类的定义可能通过类别的使用跨越几个文件。类目跨越划分类的定义或者扩展一个已经存在的。类别的描述跨越参考 “Categories and Extensions.”
源文件
通常类的接口跟实现是在两个不同的文件中,即便编译器没有强制要求。接口文件必须对任何想要使用该类的人可用。
一个文件可以声明或者实现不止一个类。不过按照惯例,通常每个类都有一个单独的接口文件,以及一个单独的实现文件。保持类接口的独立能够更好的反映它们作为独立实体的状态。
接口和实现文件都用该类命名。实现类文件的名字的扩展名是.m
, 指明它含有Objective-C 源码. 接口文件可以使用任何别的扩展。因为它是被包含在源文件中的,接口文件通常使用典型的头文件所使用的.h
扩展名。例如, Rectangle
类可以定义在Rectangle.h
以及Rectangle.m
中。
让一个对象的接口与实现独立开来的做法很好的符合了面向对象设计的思想。一个对象是一种自包含的实体,它可以从外部被当做类似黑盒一样访问。一旦你决定你的对象如何与你程序中的其它元素进行交互,也就是说,一旦你声明好它的接口,你就可以自由的修改它的实现,而不会影响到应用的其它部分了。
类接口
类接口的声明是以编译指令@interface
开头并以指令@end
结尾。 (所有的Objective-C 编译器指令都是以 “@” 开头.)
@interface ClassName : ItsSuperclass |
// Method and property declarations. |
@end |
第一行声明标示一个新类名并将其与它的父类联系起来。父类定义了新类在继承树中的位置,就像我们在“Inheritance.” 中讨论的那样。
类的方法以及属性定义在之后,类声明结尾处的前面。能被类对象使用的方法名,即类方法,方法前面会有个加号标记;
+ alloc; |
为类的实例使用的方法,即实例方法,方法前会有一个减号标记。;
- (void)display; |
尽管这不是什么惯例,你也可以给类方法和实例方法定义一样的名字。方法也可以跟实例变量名字相同,这更常见,尤其是当方法在变量中返回值时。例如, Circle
有一个 radius
方法与 radius
实例变量相匹配。
方法的返回值类型的名声使用的是标准的C语法:
- (float)radius; |
参数的类型定义也是一样的:
- (void)setRadius:(float)aRadius; |
如果一个返回值或者参数类型没有显示的被声明,那么它会假定默认类型为id。早前讲到过的 alloc
方法会返回 id
.
当有更多的参数时,所有的参数会声明在方法名内,在冒号的后面。例如:
- (void)setWidth:(float)width height:(float)height; |
有较多参数的方法,在声明这些参数时使用逗号以及省略号,就像函数那样:
- makeGroup:group, ...; |
属性声明的格式是:
@property (attributes) Type propertyName; |
更多关于属性声明的信息可以参考 “Declared Properties.”
注意: 过去,接口还要求类的实例变量的声明,数据构造器是每个类实例的一部分。它们定义在 @interface
声明后面的大括号离,方法声明之前。
@interface ClassName : ItsSuperclass |
{ |
// Instance variable declarations. |
} |
// Method and property declarations. |
@end |
导入接口
接口文件必须被包含在任何依赖这个类接口的源码模块中,即任何需要为这个类创建实例的模块,发送一个消息来调用为这个类声明的方法,或者使用定义在类中的实例变量。接口通常包含在 #import
指令中
#import "Rectangle.h" |
这个指令跟 #include
的作用是一样的,唯一的区别是这条指令可以确保同一个文件不会被导入多次。因此在Objective-C的基础文档中的例子大多使用这个指令。
为了反映一个类的定义是建立在所继承的类的定义基础上,那么这个接口文件就以导入父类的接口开始。
#import "ItsSuperclass.h" |
@interface ClassName : ItsSuperclass |
// Method and property declarations. |
@end |
这个惯例说明,每个接口文件都间接的包含所有继承的类的接口文件。当一个源文件模块导入一个类接口时,它同时也获得了整个接口树中的接口。
如果有一个 precomp—一个预编译头文件—支持父类,你可能会更愿意导入预编译的文件。
引用其它类
当一个接口文件声明了一个类,导入了它的父类,显示包含了所有继承到的类的声明,从NSObject
开始一直到它的父类。如果接口引用了不在它继承树中的类,它就必须显示的导入它们或者用@class
指令声明它们
@class Rectangle, Circle; |
这个指令简单的通知编译器, “Rectangle” 跟 “Circle” 是类名,没有导入它们的接口文件。
在接口文件中,当它静态的给实例变量,返回值以及参数赋予类型时,用到了类名
- (void)setPrimaryColor:(NSColor *)aColor; |
引用了 NSColor
类.
因为向这样声明只是简单将类名用作赋予类型,并不依赖任何接口的详情 (它的方法和实例变量), @class
指令已经可以为编译器提供足够的有关编译器能够获得什么信息预警。但是,当类的接口真正使用时(实例创建时,发送消息时), 此时类接口就必须被导入。通常接口文件会使用 @class
指令来生命类, 而真正的响应的实现文件会导入它们的接口(因为它需要为这些类创建实例并给它们发消息)。@class
指令将给编译器以及链接器看的代码量最小化 , 同时也是最简单的方式来提供类名的前置声明。让事情变的简单,它可以有效避免导入的文件本身还导入了别的文件时可能出现的潜在问题。例如,当一个类声明了两外一个类的一个静态类型的实例变量,并且他们的两个接口文件相互导入,这种情况下可能两个类都无法正确编译。
接口的职责
接口文件的职责是为其它源代码模块(以及其它程序猿)声明新类。包含它们的类协同工作需要的一些信息 (程序猿可能比较喜欢还有点文档).
-
接口文件告诉用户类是怎么接入继承树的,以及需要用到的其它的类,继承的或者在类中某个位置引用到的。
-
通过它的方法声明列表,接口文件让其它模块知道什么样的消息可以发送给类对象以及类的实例。每个能在类声明外部使用的方法都声明在接口文件中。实现类内部使用的方法可以忽略
类的实现
T类的定义就像它的声明一样,都是结构化的。以@implementation
指令开始, @end
指令结束. 另外,类还可能会在@implementation
指令后面的大括号中声明实例变量:
@implementation ClassName
|
{
|
// Instance variable declarations.
|
}
|
// Method definitions.
|
@end
|
实例变量通常都有声明的属性来指定 (可以参考 “Declared Properties”). 如果你没有额外的实例变量要声明,那么你可以忽略上面提到的大括号。
@implementation ClassName |
// Method definitions. |
@end |
注意: 每个实现文件都必须导入它自己的接口。例如Rectangle.m
类要导入Rectangle.h
. 因为实现不需要重复任何它导入的声明,它可以安全的忽略父类的名字。
类的方法的定义,就像C函数一样,有一堆大括号。在大括号前面,它们的声明跟接口文件中的习惯一样,但没有分号。例如:
+ (id)alloc { |
... |
} |
- (BOOL)isFilled { |
... |
} |
- (void)setFilled:(BOOL)flag { |
... |
} |
有多个参数的方法的处理方式就像函数那样就可以了:
#import <stdarg.h> |
... |
- getGroup:group, ... { |
va_list ap; |
va_start(ap, group); |
... |
} |
引用实例变量
默认情况下,实例方法的定义会拥有这个对象所在域的所有实例变量。 它可以简单的通过名字来引用它们。尽管编译器创建了完全等价于C的构造器来存储实例变量,但构造器的具体形态被隐藏了。你不需要任何一个构造运算符(.或者->)来引用对象的数据。例如这个方法定义引用了receiver的 filled
实例变量。
- (void)setFilled:(BOOL)flag |
{ |
filled = flag; |
... |
} |
接收对象跟它的 filled
实例变量都没有被声明成这个方法的一个参数,然而这个实例变量还是在它的作用域内。这种方法语法的简化在Objective-C 代码书写中是一种有效的速记方式.
当实例变量属于的对象不是receiver的,对象的类型必须通过静态赋予类型显式的呈献给编译器。在引用一个赋予静态类型的对象的实例变量时,将使用构造指针运算符(->
) 。
假设, Sibling
类声明了一个赋予静态类型的对象, twin
, 作为实例变量:
@interface Sibling : NSObject |
{ |
Sibling *twin; |
int gender; |
struct features *appearance; |
} |
只要赋予静态类型的对象的实例变量,在类的有效域内,(就像在这里看到的,twin被赋予了跟类一样的类型), Sibling
方法就可以直接设置它们:
- makeIdenticalTwin |
{ |
if ( !twin ) { |
twin = [[Sibling alloc] init]; |
twin->gender = gender; |
twin->appearance = appearance; |
} |
return twin; |
} |
实例变量的作用域
如果向要对象隐藏它的数据,编译器可以限制实例变量的作用域—即,限制它们在程序中的可见度。但为了提供良好的可伸缩性,它同时允许你显式的把作用域设置为4个等级,每个等级都有对应的编译器指令。
指令 |
含义 |
---|---|
|
实例变量仅在声明它的类中可被访问。 |
|
实例变量在声明它的类中以及继承它的子类中可被访问。如果一个实例变量没有任何显式的声明指令, |
|
实例变量随处都可以被访问 |
|
使用modern 运行时,(modern跟legacy runtime相关介绍可以参考此文),一个
这种作用域在框架类中的实例变量很有用,在这种地方 |
图 2-1 向我们展示了实例变量的四个级别
图 2-1 实例变量的作用域 (@package
作用域没有展示)![](http://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjectiveC/Art/scopeinstvariables.gif)
一个作用域指令可以作用到它后面的所有实例变量列表,直到下一个指令出现,或者列表结束。下面的例子中, age
和 evaluation
实例变量是私有的; name
, job
, 和 wage
是 protected; boss
是公开的.
@interface Worker : NSObject |
{ |
char *name; |
@private |
int age; |
char *evaluation; |
@protected |
id job; |
float wage; |
@public |
id boss; |
} |
默认情况下,没有标识的实例变量比如上面的 name,
默认为
@protected
.
类声明的所有实例变量,无论它们的标识是什么,都在类定义的作用域之内。例如,一个类声明了一个 job
实例变量,例如上面的 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
方法,它需要在object做任何动作时改变坐标。它可以调用 setOrigin::
方法来改变坐标,你需要做的是发送一个setOrigin::
给消息给一个对象,这个对象就是 reposition
消息自身的发送对象。当你在写 reposition 代码时, 你可以通过 self
或者 super
来引用刚提到的对象。
- reposition |
{ |
... |
[self setOrigin:someX :someY]; |
... |
} |
或者:
- reposition |
{ |
... |
[super setOrigin:someX :someY]; |
... |
} |
这里的 self
跟 super
都引用的是接收reposition
消息的对象。然而这两个东东还是有很大区别的. self
是消息例程传送给每个方法的一个隐藏参数。它是一个本地变量,并可以在一个方法实现中自由的使用。super
只有作为消息表达式的接收器时才能替代self
。作为接收器,这两个东东的主要区别在于它们是如何影响消息进程的:
-
self
按照常规搜索方法实现,开始于接收对象的类的派发表中。在上面的例子中,开始于接收 reposition 消息的对象的类. -
super
是一个标识,它告诉编译器去到一个完全不同的地方去查找这个方法实现。它开始于定义了含有super
的方法的类的父类中。(真心绕啊). 在上面的例子中,它开始于定义 reposition 的类的父类中.
只要 super
收到一条消息,编译器就会用另外一个消息例程来替代objc_msgSend
函数. 替代例程直接查找定义类的父类,即发消息给super
的类的父类
—而不是接收消息的对象的类。
看一个使用self 跟 super的例子:
当我们使用有继承关系的3个类时,self
跟 super
的区别将变的很清楚。假如,我们创建一个属于 Low 类
的对象, Low
类的父类是 Mid
;Mid
的父类是 High
. 三个类都定义了一个方法叫做 negotiate
, 每个类都处于自己的考虑去使用这个方法。另外, Mid
类定义了一个NX的方法叫做 makeLastingPeace
, 这个方法用到了 negotiate
方法. 上面提到的方法和类可以在图 2-2 中看到
![](http://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjectiveC/Art/highmidlow.gif)
假设在 makeLastingPeace
方法实现中( Mid
类中) 使用 self
来只带发送 negotiate
消息的对象:
- makeLastingPeace |
{ |
[self negotiate]; |
... |
} |
当一条发给了 Low
对象以执行 makeLastingPeace
方法, makeLastingPeace
会发哦那个一条negotiate
消息给相同的 Low
对象. 消息例程找到了定义在Low中的 negotiate
版本
, self
的类
.
但是,如果 makeLastingPeace
的实现使用super
来作为接收器,
- makeLastingPeace |
{ |
[super negotiate]; |
... |
} |
消息例程发现了定义在 High
中定义的 negotiate
版本. 而将接收 makeLastingPeace
消息的对象的类(Low
) 忽略掉并直接跳到父类Mid
中, 因为makeLastingPeace是定义在 Mid
中的. 实现也没有找到 Mid
版本的 negotiate
.
在这个例子中, super
提供了一种绕过某个方法的途径。这里,我们使用 super
以使 makeLastingPeace
绕过 Mid
中覆写了父类版本的 negotiate
的方法。
无法获取 Mid
版本的 negotiate
方法,可能看起来像个缺陷,但在某些环境下,它是有意为之的:
-
Low
类的作者故意覆写了Mid
版本的negotiate
方法,所以Low
(以及其子类) 的实例将调用覆写版本的方法。Low
的设计者并不像Low
对象执行继承来的方法。 -
Mid
的方法makeLastingPeace
方法的作者,在发送negotiate
消息给super
(就像在第二个实现中展示的), 故意跳过了Mid
版本的negotiate
(以及任何定义在继承了Mid 的
类似Low
的类) 以执行定义在High
类中的版本. 第二个makeLastingPeace
实现的设计者希望使用High
中定义的negotiate
版本而不是别的类中的。
Mid
版本的 negotiate
仍可以使用,但需要发送一个直接的消息给 Mid
实例来达到这个目的;
使用 super
发送给 super
的消息允许方法实现被分发给多个类。你可以覆写一个已经存在的方法来增加或者修改一些东西。并在修改中结合原始方法。
- negotiate |
{ |
... |
return [super negotiate]; |
} |
对于一些任务来说,继承关系中的每一个类都可以实现一个方法,这个方法会做一部分工作并把消息传给 super
完成剩下的. 能够初始化新分配的实例 的init
方法,就是设计用来像这样工作的。每个 init
方法都有自己初始化定义在类中的实例变量的职责。但在做这些之前,它要发送一个 init
消息给super
来获取继承来的类并实例化它们的实例变量。每个版本的 init
都遵循这个流程,所以类都是按照继承顺序来初始化它们的实例变量的。
- (id)init |
{ |
self = [super init]; |
if (self) { |
... |
} |
} |
我们也可以关注定义在父类中的某一个方法的核心功能,并让子类把这个方法通过 super
并入这个方法。例如,每个创建实例的类方法都必须为新对象分配存储单元并初始化它类构造器的 isa
变量. 分配通常留下给定义在NSObject
类中的 alloc
和
allocWithZone:
方法来做。如果两外一个类覆写了这些方法 (一种少见的情况), 它仍然可以通过发送消息给super
获取基础功能。
重定义 self
super
只是一个简单的标识,告诉编译器应该从哪里查找要执行的方法。它仅用于消息的接收器。但 self
则是一个变量名,使用的方式很多,甚至可以赋予一个新值。
有一种情况就是定义在类方法中。类方法通常并不关心类对象,而是类的实例。例如,许多类方法集成了一个实例的 allocation 和 initialization ,通常同时设置实例变量的值。在这种方法中, 可能忍不住会发消息给新分配的实例并调用实例 self
, 就像在实例方法中那样。但这样做是错误的。self
和 super
都both 指向接收的对象—该对象收到一条消息说让他执行这个方法。在一个实例方法内部, self
指向的是实例。但在类方法的内部,self
指向的是类对象。我们不能像下例这样做:
+ (Rectangle *)rectangleOfColor:(NSColor *) color |
{ |
self = [[Rectangle alloc] init]; // BAD |
[self setColor:color]; |
return self; |
} |
为了避免困惑,在类方法中,通常更好的做法是使用一个变量而不是 self
来指代一个实例:
+ (id)rectangleOfColor:(NSColor *)color |
{ |
id newInstance = [[Rectangle alloc] init]; // GOOD |
[newInstance setColor:color]; |
return newInstance; |
} |
事实上,与其发送 alloc
消息给类方法,不如发送alloc
给 self
. 这种方式下,如果类是子类,并且 rectangleOfColor:
消息被子类接收到,这个实例返回的类型会与子类相同(例如, NSArray
的 array
方法被NSMutableArray
所继承)。
+ (id)rectangleOfColor:(NSColor *)color |
{ |
id newInstance = [[self alloc] init]; // EXCELLENT |
[newInstance setColor:color]; |
return newInstance; |
} |
可以参考 “创建并初始化对象” 获取更多实现初始化相关的方法。