Objective-C & Sprite Kit太空历险记 : 4. 打造作战单位——面向对象编程(下)

本文深入探讨Objective-C中的类与对象概念,包括初始化方法、继承、分类、对象复制及动态处理等内容,帮助读者理解面向对象编程的核心原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文

  http://www.ituring.com.cn/article/213627

  

4.5. 初始化方法

我们先回忆一下是如何创建五号机器人对象的。

CRobot *robot5 = [[CRobot alloc] init];

代码中,我们初始化对象robot5时,实际上调用了两个方法,即alloc和init。其中,alloc方法用于创建一个基本的CRobot对象,接下来的init方法就是对象数据的初始化方法。

实际上,在Objective-C中约定,在类中以init开头的方法都会假设为对象的初始化方法,所以,在我们对自定义的方法命名时,要特别注意这一点。

我们创建的类,同样可以定义自己的初始化方法,如下面的代码,我们在接口部分声明初始化方法。

@interface CRobot: NSObject
-(instancetype) initName:(NSString*)n andSpeed:(float)s;
@end

然后,我们在类的实现部分定义这个初始化方法,如下面的代码。

@implementation CRobot

-(instancetype) initName:(NSString*)s andSpeed:(float)s
{
    self = [super init];
    if (self) {
        self.name = n;
        self.speed = s;
    }
    return self;
}
@end

首先,我们先看一下在代码中新出现的三个关键字:

  • instancetype,我们指定初始化方法的返回值为instancetype类型,表示此方法会返回当前类类型的对象(实例)。
  • super,表示基类(父类、超类)对象。
  • self,表示当前对象。

这三个关键字的作用,我们会在继承一节中详细介绍。

现在,我们回到初始化方法的应用当中。下面的代码,我们将演示这个自定义初始化方法的使用。

CRobot *robot5 = [[CRobot alloc] initName:@"五号" andSpeed:50.0];
NSLog(@"%@ 的速度是 %f km/h", robot5.name, robot5.speed);

我们看到,使用初始化方法,可以简化很多的对象初始代码,而且,通过多个初始化方法,可以快速创建多种形式的对象,可以在很大程度上提高开发效率。

使用自定义初始化方法,有一些问题需要我们注意:

  • 初始化方法的返回值类型应该定义为instancetype。请注意,在早期的Objective-C代码中,初始化方法返回值通常定义为id类型。
  • 初始化方法都应该以init开头。
  • 初始化方法的参数定义与普通方法的参数定义相同。
  • 在初始化方法中,一般情况下,都应该首先使用super关键字调用基类的初始化方法,以保证初始化工作的完整性。
  • 为了简化代码,我们也可以在初始化方法中调用本类中其它的初始化方法,此时,应使用self(当前实例)调用,如[self init]。

4.6. 继承

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

4.6.1. 成员的访问

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

  • super关键字用于在子类中访问基类(父类、超类)中的成员,包括属性和方法等。但是请注意,这并不包括基类中定义为私有的(private)成员。
  • 在接口部分声明的属性和方法默认可访问是公共的(public),而在实现部分定义的方法的默认可访问性是受保护的(protected)。
  • 对于实例变量,定义在接口部分的实例变量默认可访问性是受保护的,可以在本类或子类中访问,但我们可以使用@public、@protected和@public指令修改它们的可访问性。实现部分定义的实例变量,其可访问性默认则私有的,只能在本类中使用。
  • self关键字用于访问当前对象,我们可以在类中使用这个关键字访问当前对象的各种属性和方法。

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

#ifndef __CRobotSolider_h__
#define __CRobotSolider_h__

#import <Foundation/Foundation.h>
@interface CRobotSolider : CRobot
@property NSString* weapon;
-(void) fire;
@end

#endif

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

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

我们可看到,在CRobotSoldier类中的fire方法中,使用self关键字了name和weapon属性,其中weapon属性为CRobotSoldier类中定义的属性,相信不难理解,而name则是在CRobot类中定义的属性,只是CRobotSoldier类继承于CRobot类,所以,我们也可以在CRobotSoldier类中使用name类;如下面的代码。

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

4.6.2. 重写属性和方法

O博士:前面,我们已经讨论了如何使用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];

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

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

4.6.3. 继承关系中的初始化

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

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

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

对象初始化完整性

O博士:在初始化方法一节中,我们提到过初始化方法来保证对象初始化的完整性,所以,当我们在子类中定义了新的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]语句调用它了。

id与instancetype

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

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

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

4.7. 分类

O博士:当我们的机器人士兵刚刚投入战斗时,发生一件很不爽的事情,敌人将我们的机器人士兵捕获后重写程序,并用于突袭我们,造成了一些不必要的损失;由于我们不可以能将所有战斗中的机器人都返回工厂进行修改程序,所以,我们就制造了一个小小的辅助装置,我们称它为分类(category),使用分类可以不修改原类,也不使用继承功能,就可以扩展原有类的功能,而我们在机器人士兵上使用的分类就是一个自毁(self-destruct)装置。下面,我们就来了解分类的应用。

4.7.1. 命名分类

开发中,如果我们要创建一个类的分类,需要创建一组新的头文件和代码文件;比较常用的命名方式是“原类名+分类名”。如我们创建CRobotSoldier的自爆功能分类,可以使用CRobotSoldierSelfDestruct分类,下面就是分类的头文件部分(CRobotSoldierSelfDestruct.h文件)。

#ifndef __CRobotSoldierSelfDestruct_h__
#define __CRobotSoldierSelfDestruct_h__

#import <Foundation/Foundation.h>
#import "CRobotSoldier.h"

@interface CRobotSolder(SelfDestruct)
-(void) selfDestruct;
@end

#endif

接着,我们需要在相应的分类代码文件中实现新的成员(CRobotSoldierSelfDestruct.m文件)。

#import "CRobotSoldierSelfDestruct.h"

@implementation CRobotSolder(SelfDestruct)
-(void) selfDestruct
{
    NSLog(@"启动自爆装置");
}
@end

然后,在代码中,我们可以通过下面的代码来使用分类中新添加的成员。

#import <Foundation/Foundation.h>
#import "CRobotSoldierSelfDestruct.h"

int main(int argc, char *argv[])
{
    @autorealeasepool {
        CRobotSoldier *killer = [[CRobotSoldier alloc] init];
        [killer selfDestruct];
    }
    return 0;
}

请注意,在使用分类时,我们直接定义的是原类(如CRobotSoldier)的对象。

此外,在分类中也可以定义与原类中同名的成员,但这样一来,分类中的成员就会完全覆盖原类中的成员,而且原类中的成员再无法访问;除非你的目的就是这样,否则需要小心使用。

4.7.2. 匿名分类

O博士:在创建原类的分类时,我们还可以不指定分类名称,此时,在分类接口部分只需要在类名后面跟着空的一对圆括号()即可。不过,应注意匿名分类的实现部分必须放在原类的实现代码文件中,也就是说要和原类的实现代码在一起。

4.8. 类与对象的应用

O博士:我们已经讨论了在Objective-C中如何创建类、如何创建类的实例(对象),以及类的继承等相关主题;在本章接下来的内容中,我们将会讨论一些关于类和对象的应用问题。

4.8.1. 对象的复制

下面的代码,我们将使用CRobot类来演示对象的复制问题。

CRobot *robot5 = [[CRobot alloc] init];
robot5.name = @"No.5";
CRobot *robot6 = [[CRobot alloc] init];
robot6 = robot5;
NSLog(@"%@ , %@\n", robot5.name, robot6.name);
//
robot6.name = @"No.6";
NSLog(@"%@ , %@\n", robot5.name, robot6.name);

我们可以看到,第一个NSLog()函数显示为“No.5 , No.5”;然后,当我们修改robot6.name的值以后,第二个NSLog()函数显示为“No.6 , No.6”,也就是说,当我们修改robot6对象的值时,robot5对象的值也“变化”了,这是为什么呢?

实际上,我们说过,对象就是指针!当我们将一个对象赋值给另一个对象时,实际执行的是“浅复制”,也就是复制了对象的指针,这样,代码中的robot5和robot6对象实际上是指向同一内存区域;所以,当我们修改其中一个对象的值,实际会同时反映到两个对象变量。

代码中,如果我们需要完全复制一个全新的对象,即对象的“深复制”操作,有两个方法,一是通过实现NSCopying协议,另一个方法就是通过归档来实现,稍后,我们会讨论相关内容。

4.8.2. 对象作为参数

我们已经看到对象在赋值时的默认表现,即进行浅复制,而这一特性在对象作为函数或方法的参数时也会有着相同的表现。

当通过对象向函数或方法中传递数据时,实际上传递的是对象的引用,此时,在函数或方法中对这个对象的操作应该注意:

  • 通过引用传递对象,可以提高数据的传递效率,因为只传递指针,而不需要复制全部数据。
  • 在函数或方法中对于对象的修改,会直接反映到外部对象,应注意代码的目的是否确实是这样。

4.9. 动态处理类和对象

O博士:在机器人士兵控制系统的开发过程中,可能需要判断它们的功能,这就需要动态的判断对象或类所支持的方法等成员,这些操作都可以通过NSObject类中定义的一系列方法来实现,由于我们创建的类最终都会以NSObject类为超级基类,所以,我们可以在所有类中使用这些方法。接下来,我们就讨论一些常用的内容。

4.9.1. 对象类型判断

当我们需要判断一个对象是否是某个类的实例时,可以使用如下方法:

-(BOOL) isMemberOfClass: classObject;

我们可以看到,这是一个实例方法,其中的参数classObject为类对象,我们可以使用类方法class获取,如“[CRobot robot5]”。下面的代码,我们将判断一个对象是否为CRobot类的实例(对象)。

CRobot *robot5 = [[CRobot alloc] init];
BOOL result = [robot5 isMemberOfClass: [CRobot class]];

除了判断一个对象是否为某个类的直接实例,我们还可以判断一个对象是否为某个类或其基类的实例,如下面的方法:

-(BOOL) isKindOfClass: classObject;

下面的代码演示了此方法的使用。

CRobotSoldier *killer = [[CRobotSoldier alloc] init];
BOOL result = [killer isKindOfClass: [CRobot class]];

由于CRobotSoldier是CRobot的子类,所以,result的值也是YES。

除了判断对象的类型,我们还可以判断类的继承关系,如下面的实例方法isSubclassOfClass就可以判断一个类是否为某类的子类。

+(BOOL) isSubclassOfClass: classObject;

如下面的代码,我们将判断CRobotSoldier是否为CRobot类的子类。

BOOL result = [CRobotSoldier isSubclassOfClass: [CRobot class]];

4.9.2. 方法是否存在

我们知道,在类中定义的方法分为实例方法和类方法,我分别使用如下两个方法进行判断。先看如何判断实例方法是否存在,我们使用respondsToSelector方法,其定义如下:

-(BOOL) respondsToSelector:selector;

其中的参数selector为SEL类型(选择器类型),我们使用@selector(<方法名>)获取没有参数的方法的SEL类型对象。如下面的代码,我将判断CRobot类型的对象中是否可以使用work方法。

CRobot *robot5 = [[CRobot alloc] init];
BOOL result = [robot5 respondsToSelector:@selector(work)];

如果我们需要判断的方法包含有参数,那么,在@selector()指令中只需要包括方法名、参数名称和冒号即可,并不需要指定参数变量。

判断一个类是否包含类方法时,可以使用如下方法:

+(BOOL) instancesRespondToSelector:selector;

其使用与respondsToSelector:方法类似。

4.9.3. 动态调用方法

代码中,如果我们需要动态调用方法,可以使用如下三个实例方法,分别用于调用无参数、一个参数和两个参数的方法。

-(id) performSelector:selector;
-(id) performSelector:selector withObject:object;
-(id) performSelector:selector withObject:object1 withObject:object2;

这几个方法都会返回id类型的数据,它们是调用方法的返回结果。如下面的代码就是调用CRobot对象中的move方法,此方法没有返回值,所以,我们也不需要处理performSelector:方法的返回值。

CRobot *robot5 = [[CRobot alloc] init];
robot5.name = @"No.5";
[robot5 performSelector:@selector(move)];
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值