Objective-C和Sprite Kit游戏开发从入门到精通
上QQ阅读APP看书,第一时间看更新

4.6 继承

继承最大的好处就是代码的复用,在Objective-C代码中使用类的继承体系,有一些问题需要我们注意,如成员的访问级别、属性和方法的重写、初始化方法的继承等。本节就来讨论这些问题。

■4.6.1 成员的访问

在讨论类的初始化方法时,我们已经看到了一些成员访问相关的内容,如:

❑ super关键字用于在子类中访问父类(基类、超类)中的成员,包括属性和方法等。但是请注意,这并不包括父类中定义为私有的(private)成员。

❑ 在接口部分声明的属性和方法,其访问级别是公共的(public),而在实现部分定义的方法的默认访问级别是受保护的(protected),它们可以在子类中访问。

❑ 对于实例变量,定义在接口部分的实例变量默认访问级别是受保护的,可以在本类或子类中访问,但我们可以使用@public、@protected和@public指令修改它们的访问级别。实现部分定义的实例变量,其访问级别默认则私有的,只能在本类中使用。

❑ self关键字用于访问当前对象,我们可以在类中使用这个关键字访问当前对象的各种属性和方法。

接下来,我们会在标准机器人的基础上创建机器人士兵,定义为CRorotSoldier类,它将继承CRobot类,其接口部分如下面的代码(CRobotSoldier.h文件)。

    #ifndef __CRobotSoldier_h__
    #define __CRobotSoldier_h__
    #import <Foundation/Foundation.h>
    @interface CRobotSoldier : CRobot
    @property NSString* weapon;
    -(void) fire;
    @end
    #endif

接下来是CRobotSoldier类的实现部分,如下面的代码(CRobotSoldier.m文件)。

    #import "CRobotSoldier.h"
    @implementation CRobotSoldier
    @synthesize weapon;
    -(void) fire
    {
        NSLog(@"机器人%@使用%@开火", self.name, self.weapon);
    }
    @end

我们可看到,在CRobotSoldier类中的fire方法中,使用self关键字调用了name和weapon属性,其中weapon属性为CRobotSoldier类中定义的属性,而name属性是在CRobot类中定义的,但由于CRobotSoldier类继承于CRobot类,所以,我们也可以在CRobotSoldier类中使用name类。

下面的代码演示了CRobotSoldier类的使用。

    CRobotSoldier *killer = [[CRobotSoldier alloc] init];
    killer.name = @”Killer-1”;
    killer.weapon = @”脉冲枪";
    [killer fire];

■4.6.2 重写属性和方法

前面,我们已经讨论了如何使用super和self关键字分别调用父类或本类的成员。在开发中,有些时候可能需要在子类中完全重写父类中的成员;在Objective-C中,这个工作很简单,只需要在子类的实现部分创建一个完全一样的成员,就可以覆盖基类中的同名成员。

如下面的代码(CRobotSoldier.m文件),我们将在CRobotSoldier类的实现部分重写work方法。

    @implementation CRobotSoldier
    // 其他代码
    -(void) work
    {
        NSLog(@"机器人%@正在战斗", self.name);
    }
    @end

在下面的代码中,我们直接使用CRobotSoldier类中的work方法。

    CRobotSoldier *rs = [[CRobotSoldier alloc] init];
    rs.name = @"Killer-1";
    [rs work];

当子类中重写了父类的成员以后,我们还是可以在子类中使用super关键字访问到父类中的同名成员。如下面的代码。

    @implementation CRobotSoldier
    // 其他代码
    -(void) work
    {
        [super work];
        NSLog(@"机器人%@正在战斗", self.name);
    }
    @end

■4.6.3 继承关系中的初始化

在类的继承关系中,了解初始化方法的调用关系非常重要,在前面的示例中,我们并没有在CRobotSoldier类中定义初始化方法,那么,当我们执行如下面代码时,初始化方法是怎么工作的呢?

    CRobotSoldier *rs = [[CRobotSoldier alloc] init];

实际上,当我们调用初始化方法init时,代码会从当前类向上(父类)的顺序开始查找初始化方法,也就是说,此代码中的init方法的查找顺序应该是CRobotSoldier→CRobot→NSObject。由于CRobotSoldier和CRobot类都没有定义init方法,所以,最终调用的就是NSObject类中的init方法。

1.对象初始化完整性

在4.5节中,我们提到,初始化方法的调用应保证对象初始化的完整性,所以,当我们在子类中定义了新的init方法以后,一般情况下,还应该首先调用基类的init方法以完成对象的前期初始化工作,如下面的代码。

    -(instancetype) init
    {
        self = [super init];
        // 当前对象初始化代码
        return self;
    }

接下来,我们将在CRobot和CRobotSoldier类中创建init初始化方法,然后,我再来观察它们的调用顺序。

首先,在CRobot类中重写init方法,如下面的代码(CRobot.m文件)。

    @implementation CRobot
    // 其他代码
    -(instancetype) init
    {
        self = [super init];
        if (self) {
            NSLog(@"正在组装机器人");
        }
        return self;
    }
    @end

下面是CRobotSoldier类中重写的init方法(CRobotSoldier.m文件)。

    @implementation CRobotSoldier
    // 其他代码
    -(instancetype) init
    {
        self = [super init];
        if (self) {
            NSLog(@"正在改造机器人士兵");
        }
        return self;
    }
    @end

下面的代码,我们观察这几个初始化方法调用的情况。

    CRobotSoldier *killer = [[CRobotSoldier alloc] init];

当我们调用CRobotSoldier类的init方法初始化对象时,实际会调用三个init方法,它们的调用顺序是[NSObject init]→[CRobot init]→[CRobotSoldier init],这样,我们就不难看出这一行代码会输出什么内容了,即:

    正在组装机器人
    正在改造机器人士兵

请注意信息的顺序,这实际显示了初始化方法调用的关系。此外,[NSObject init]方法并不是我们定义的,而且没有显示信息,但应注意,在CRobot类中的init方法中,我们的确使用[super init]语句调用它了。

2. id与instancetype类型

如果看到较早版本的Objective-C代码,你可能会发现类的初始化方法返回值类型被定义为id类型,而这个类型可以存放任意类型的对象。那么id和instancetype类型有什么区别呢?

首先,我们可以理解instancetype实际上是初始化方法的专用关键字,它只用于定义初始化方法(类方法或实例方法)的返回值类型;它的含义是,本方法返回的结果是方法所在类的实例(对象)。使用instancetype关键字,可以明确方法的作用和目的,在编译或运行时都能够更有效地发现对象初始化过程中可能出现的问题。

id类型表示任意类型的对象,在代码中,和其他类型一样可以定义对象、方法的返回值,或者是参数类型等,所以,id类型的应用会更灵活,但同时也应该非常注意,因为从字面上看,它的类型是不明确的。但是不用着急,关于如何动态地处理类和对象,本章稍后会有讨论。