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)];