继承的概念

父类和子类

我们在定义一个新类的类的时候,经常会遇到要定义的新类是某个类的扩展或者是对某个类的修正。这种情况,如果可以在已有类的基础上追加内容来定义新类,那么新类的定义将会变得更简单。
向这种通过扩展或者修改即有类来定义新类的方法叫继承(inheritance)。在继承关系中,被继承的类叫父类,通过继承关系新建的类称为子类。
继承意味着子类继承父类的所有特性,父类的数据成员和成员函数自动成为子类的数据成员和成员函数。除此之外,子类还可以:

  • 追加新的方法
  • 追加新的实例变量
  • 重新定义父类的方法
    子类中重新定义父类的方法的行为叫做重写。为了避免命名上的混乱,C++中也将父类称为基类 ,把子类称为派生类导出类

类的层次结构

假如以某个类为父类生成若干子类,然后再继承这些子类并生成更多的子类,如此循环下去就可能会生成一棵倒立的树,它由通过继承而彼此关联的类组成,这样的树称为类层次结构。位于类层次最顶端的类称为根类
NSObject是Cocoa环境下的根类,Cocoa中所有的类都直接或间接地继承NSObject。新建的任何类都必须是NSObject或它的继承类的子类。NSObject中定义了所有Objective-C对象的基本方法。

利用继承定义新类

继承的定义

1
2
3
4
5
6
7
@interface 类名 : 父类名
{
实例变量的声明;
...
}
方法声明;
...

Objective-C中所有的类都要继承根类,而NSObject是Objective-C中所有类的根类。如果子类有想继承的类,就要直接指明该类为父类,否则就需要指定NSObject为父类。

类定义和头文件

接口部分通常被声明为一个头文件,而这对继承来说也很重要。假设有一个已经定义好的类Alpha,那么头文件Alpha.h就应该已经存在。要定义类Alpha的子类Beta的时候,头文件Beta.h中必须包含Alpha.h。不知道父类定义的话是无法定义子类的。所以包含父类接口的头文件是必须的
类的实现部分必须引入包含类的接口部分的头文件。实现部分需要包含新增和重写的方法的实现。当然实现部分也可以定义各种局部函数和变量。
类的定义可以不断地往下扩展,但无论怎么扩展,只要保证了这种头文件的引入方式,任何一个派生类中就能使用父类中定义的变量和方法。

调用父类的方法

子类继承父类之后,如果希望调用父类的方法,可以通过super关键字来发送消息。使用super发送消息后,就会调用父类或父类的父类中定义的方法。

1
[super method];

super和self不同,并不确定指向某个对象。所以super只能被用于调用父类的方法,不能通过super完成赋值,也不能把方法的返回值指定为super。

初始化方法的定义

新追加的实例变量有时需要被初始化。另外,子类也可能需要同父类不同的初始化方法。这些情况下就需要为子类定义自己的初始化方法。
子类中重写init初始化方法的时候,通常按照一下逻辑:

1
2
3
4
5
6
7
8
- (id)init
{
self = [super init] /* 一定要在第一行调用父类的init方法 */
if (self != nil) {
子类专有的初始化操作
}
return self;
}

如果所有类的初始化方法都遵循先调用父类init方法的写法,那么根类NSObject的init方法就一定会被执行。否则生成的对象就无法使用。与此同时,这样做也可以防止漏掉父类中定义的实力变量的初始化。
执行的时候父类的初始化方法可能会出错。出错时则返回nil,这种情况下子类也不需要再进行初始化,直接返回nil就可以了。
生成实力对象的方法alloc会把实力对象的变量都初始化为0(后续会提到实力变量isa除外)。所以,如果子类中新追加的实例变量的初值可以为0,则可以跳过子类的初始化。但是为了明确是否可以省略,最好为初值可为0的变量加上注释。
从程序的书写角度来说,设定初始值的方法有两种,既可以在初始化方法中一次性完成实例变量的初始化,也可以在初始化方法中先设置实例变量为默认值,然后再调用别的方法来设置实例变量。

使用继承的程序示例

追加新方法的例子

我们定义一个带有静音功能的类MuteVolume。该类只有一个功能,即当收到mute消息时,设置音量为最小

1
2
3
4
5
6
// MuteVolume.h
#import "Volume.h"

@interface MuteVolume
- (id)mute;
@end
1
2
3
4
5
6
7
8
9
10
11
12
// MuteVolume.m
#import "MuteVolume.h"

@implementation MuteVolume

- (id) mute
{
val = min;
return self
}

@end

方法重写的例子

假设该例子要实现两个功能。第一个功能是,当再次收到mute消息时,音量会恢复原职;第二个功能是,在静音状态下收到up或者down消息时,会返回最小音量,同时改变音量值

1
2
3
4
5
6
7
8
9
10
11
// MuteVolume.h
@interface MuteVolume : Volume
{
BOOL muting;
}
/* override */
- (id)initWIthMin: (int)a Max: (int)b step: (int)s;
- (int)value;
- (id) mute;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// MuteVolume.m
#import "MuteVolume.h"

@implementation MuteVolume
/* override */
- (id)initWithMin: (int)a max: (int)b step: (int)s
{
self = [super initWithMin:a max:b step:s];
if (self != nil)
muting = NO;
return self;
}
/* override */
- (int)value
{
return muting ? min : val;
}

- (id)mute
{
muting = !muting;
return self;
}

@end

继承和方法调用

使用self调用方法:
如果想在一个方法中调用当前类中定义的方法,可以利用self。但如果存在继承关系,通过self调用方法时需要格外注意:self始终指向收到当前消息的示例变量

使用super调用方法:
super调用的是父类的方法,而至于到底调用了哪个方法则是由编译时类的继承关系决定的。

我们用一个简单的程序来验证一下上面所描述的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#import <Foundation/NSObject.h>
#import <stdio.h>

@interface A: NSObject
- (void)method1;
- (void)method2;
@end

@implementation A
- (void)method1 { printf("method1 of Class A\n"); }
- (void)method2 { printf("method2 of Class A\n"); }
@end

@interface B : A
- (void)method;
@end

@implementation B
- (void)method2;
@end

@implementation B
- (void)method2 {
printf("method2 of Class B\n");
printf("self ---> ");
[self method1];
printf("super ---> ");
[super method2];
}
@end

@interface C : B
- (void)method1;
@end

@implementation C
- (void)method1 { printf("method1 of Class C\n"); }
@end

int main(void)
{
id x = [[B alloc] init];
id y = [[C alloc] init];
printf("--- instance of B ---- \n");
[x method1];
[x method2];
printf("--- instance of C --- \n");
[y method1];
[y method2];
return 0;
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
--- instance of B ---
method1 of Class A
method2 of Class B
self --> method1 of Class A
super --> method2 of Class A
--- instance of A ---
method1 of Class C
method2 of Class B
self --> method1 of Class C
super --> method2 of Class A

方法定义时的注意事项

局部方法

实现接口声明中的方法时,可以把具备独立功能的部分独立出来定义成子方法。一般情况下,这些方法都只供内部调用,不需要包含在类的接口中对外公开。这些方法,被称为局部方法。局部方法只能在被定义在局部方法之后的方法调用。定义顺序方法出现的问题,可以在后续的[[范畴]]中解决