http://www.ituring.com.cn/article/213488
各位都已经是初级军官了,为了迎接更多的挑战,大家必须了解太空舰队中的各种作战单位,当然,最好的方法就是了解它们的制造过程,这也是我们来到α科研中心的目的,我们将亲自加入到作战单位的研发和制造工作当中。在这里,我们将开始讨论Objective-C的真正强大的方面,即面向对象编程(OOP),主要内容包括:
- 面向对象编程基础
- 定义类
- 方法
- 属性
- 初始化方法
- 对象的释放
- 继承
- 分类
- 类与对象的应用
- 动态处理类和对象
4.1. 面向对象编程基础
O博士:现在,由我带领大家进行本部分分的的实习任务,我们将打造一个机器人控制系统,而且是要求模块化设计,这样才能更有效地控制与扩展。首先,大家可以试着使用前面学习到的内容进行相关的工作。首先,我们需要一个机器人,它应该是什么类型的呢?
Robot类型,怎么样?
实际上,我们现在还没有这个类型,那就先假装有一下得了!现在,我们需要一个机器人变量,如下面的代码。
Robot robot5;
接下来,让机器人robot5走两步看看,对于动作处理,到现在为止只能使用函数,如下面的代码。
robot_move(robot5, 2);
发现敌人要开火,如下面的代码。
robot_fire(robot5, target);
O博士:现在,大家还有兴趣继续玩下去吗?我们换一种方式玩吧。首先,还是假设有Robot类型和一个robot5变量,如果能使用下面的代码来完成移动、开火等操作是不是更直观呢。
Robot *robot5; // 为什么要加*,这可不是为了装,稍后道来
robot5.name = @"No.5"; // 机器人5号,5号活了
[robot5 move:2]; // 走两步
[robot5 fire:target]; // 射击目标
O博士:这就是面向对象编程的基本形式,我们将一个事物的特性(属性)和动作(方法)进行封装,从而能够更加直观地编写代码,同时,也会使代码更具有逻辑性、可读性,以及可维护性。
接下来,我们就先来了解一些面向对象编程的基本概念,稍后我们讨论如何在Objective-C项目中实现面向对象编程。
类与对象
O博士:在面向对象编程概念中,我们可以将“类(class)”看作为一个复杂的数据类型,就是比结构更复杂的数据类型。有了一定的数据类型,我们就可以创建这些类型的变量,而基于类类型的变量就是对象(object),或者称为类的实例(instance)。当然,我们创建对象的过程并不会像操作基本数据类型那么简单,在这一过程中,往往需要一些内存分配、数据初始化,以及资源调用等操作,这些操作都会在创建对象时进行初始化,这些创建对象的过程,我们又可以称为类的实例化。
假如我们定义了机器人类型CRobot类,我们可以创建一个具体的机器人对象,如“5号”机器人;那么,“5号”就是CRobot类的一个实例,或者说是CRobot类型的对象。
请注意,在类类型前加一个大写的C是我的编程习惯,你可以在一定的范围内根据自己的习惯和项目和约定、标准对类进行命名。
属性与方法(任务)
对象的特性,如速度、颜色、名称、位置等等,我们都可以通过属性来表示,如robot1.name=@"No.5"中,name属性就表示机器人的名称。
对象的动作,如开火、移动等等,我们就可以定义为方法来实现,如[spaceship fire]。请注意,方法(method)是面向对象编程概念中的术语,但Apple的官方文档称为任务(task),在本书中,我们会使用习惯性的面向对象编程术语,即方法(method)。在Objective-C中,它的定义方式和函数还是很大区别的,稍后我们就会看到。
继承
O博士:前面我们提到过,一个对象的创建需要有内存分配等初始化操作,如果每一个类的全部操作都需要自身的代码来实现,这些工作可不是闹着玩的;还好,我们不必要一切从零开始。
在Foundation框架中定义了NSObject类,我们定义的类,如果没有特殊要求,都可以继承NSObject类型;这样,就可以使用NSObject类中定义的成员,如属性和方法,从而简化了创建类的工作量。
从另一方面来看,在创建复杂的应用程序时,我们可以利用很已经存在的类,包括Cocoa中的资源,或者是自定义的资源,从而进一步提高开发效率。
比如,我们创建了一个汽车类CAuto,一些衍生车型就可以继承它的基本特点和功能,如CCar、CSuv等。此时,CAuto类就是CCar和CSuv的基类(又称为父类或超类),而CCar和CSuv就是CAuto的子类。
在Objective-C项目中,NSObject类是唯一没有基类的类,而其它的类,特别是我们自定义的类,都必须指定一个基类。
了解了面向对象编程的一些基本概念以后,我们就来看看如何在Objective-C中实现它们。
4.2. 定义类
O博士:现在,我就要创建真正的机器人类了。不过,我们还是先了解一些在Objective-C中定义类的基础知识。
与大多面向对象编程语言最大的区别就在于,在Objective-C中并不使用class关键字来定义类,而是使用两个部分来定义类,包括接口(interface)部分和实现(implementation)部分。
理论上讲,我们可以将类的接口部分、实现,以及应用代码都放在一个文件中,但我们知道,大多数情况下,定义一个类是为了提供给别的代码重复使用的,所以,我们一般会将接口部分定义在头文件(.h),而将实现部分定义在相应的Objective-C模块文件(.m)中。
4.2.1. 接口部分
本章的代码,我们将继续使用前面创建的项目OCDemo;在XCode菜单,通过"File"->"New"->"File",选择OS X下的Source,然后选择Cocoa Class,接下来,需要我们指定类的名称(Class)及其基类(Subclass of),如下图。
单击“Next”按钮,指定代码保存的路径以后,Xcode会为自动为我们创建同名的头文件和代码文件,我们首先看头文件中的接口部分,如下面的代码(CRobot.h文件)。
#ifndef __CRobot_h__
#define __CRobot_h__
#import <Foundation/Foundation.h>
@interface CRobot : NSObject
-(void) move;
@end
#endif
我们可以看到,类的接口部分定义在@interface和@end指令之间,而类的名称则定义在@interface指令后面,紧跟其后的冒号(:)含义为继承,本例中,我们定义的CRobot类继承于NSObject类。
在CRobot类中,我们声明了一个名为move的实例方法,它没有返回值(使用void关键字声明);接下来,我们看一看如何在实现部分具体定义这个方法。
4.2.2. 实现部分
下面的代码,我们将在CRobot.m文件中看到CHello类的定义。
#import "CRobot.h"
@implementation CRobot
-(void) move
{
NSLog(@"机器人移动");
}
@end
在这里,我们可以看到,类的实现部分定义在@implementation和@end指令之间;在@implementation指令后需要类的名称,但不需要再次指定继承哪个类。
在类的实现代码中,我们定义了move方法的具体实现,它的功能很简单,只是显示一条信息。
请注意,在类中定义的方法并没有使用小括号()来包含参数;实际上,其参数的定义和函数有所不同,稍后,我们会详细介绍方法及参数的定义。
4.2.3. 创建对象(实例化)
下面的代码,我们演示了如何在main()函数中使用CRobot类。
#import <Foundation/Foundation.h>
#import "CRobot.h"
int main(int argc, const char *argv[])
{
@autoreleasepool {
CRobot *robot5 = [[CRobot alloc] init];
[robot5 move];
}
return 0;
}
代码中,我们使用#import指令引用了CRobot.h文件,从而将CRobot类的定义引用到当前代码文件。在main()函数中,我们使用如下代码创建了一个CRobot类的实例,即robot5对象。
CRobot *robot5
大家可以看到,对象实际是被定义为指针类型的,都说了加*不是用来装的。^_^
接下来,给这个对象赋值的代码实际上是完成了对象的实例化过程,这个过程共调用了两个方法,即alloc和init方法,也许大家会问,我们并没有定义这两个方法,它们是从哪里来的呢?答案就是,它们是从继承NSObject类而来;也就是说,这两个方法是定义在NSObject类中的,而在CRobot类中,由于它是NSObject类的子类,所以,我们可以使用NSObject类中定义的这两个方法。
请注意,并不是基类中所有成员都能被子类访问的,稍后我会讨论相关主题。
最后,我们调用了robot5对象的move方法,在Objective-C中,类和对象方法的调用,其基本格式如下:
[<类或对象> <方法名和参数>];
调用类和对象中的方法时,需要使用一对方括号[]包含起来,如果你学习过C#或Java等编程语言,可能对这种方法的调用格式有些不适应,不过,用着用着也就习惯了。^_^
4.2.4. 类的成员
O博士:我们已经讨论了如何定义一个简单的类和一个方法,并在它的实例(对象)中应用。实际开发中,类的成员定义会是一项比较复杂的工作;本节,我们就来讨论一些关于类成员定义的基础知识。
属性和方法
定义一个类时,我们对外部代码提供的公共接口成员主要包括属性和方法,属性用于定义对象的特性,而方法(任务)则是定义对象可执行的动作。稍后,我们会详细讨论属性和方法的创建和使用。
实例方法和类方法
在前面定义的CRobot类中,我们定义的move属于实例方法,使用减号(-)定义,如:
-(void) move;
实例方法的特点时,它必须由对象,即类的实例来调用。
与实例方法相对应的类方法,它由类来调用,类方法使用加号(+)定义,如:
+(void) methodName;
调用类方法时,我们直接使用类的名称,如:
[CRobot methodName];
更多关于方法定义的内容稍后讨论。
实例变量
在类中,我们还可以定义一些实例变量,这些变量可以在实例方法中直接调用,并可以通过@public、@private、@protected指令指定它们的适用范围(称为作用域 、可访问性或访问级别)。
我们可以在类的接口部分或实现部分定义实例变量,此时,应在紧跟接口或实现指令后的一对花括号{}之间,如下面的代码:
@interface CRobot : NSObject
{
int counter;
}
-(void) move;
@end
在接口部分定义的实例变量,其默认使用范围(作用域)是@protected,即这些变量可以在当前类及其子类中的实例方法中使用。
实现部分定义的实例变量,只能用于当前类中定义的实例方法,相当于@private作用域。如下面的代码。
@implementation CRobot
{
int counter;
}
// 其它代码
@end
如果想简单点,可以直接在接口部分定义全部的实例变量,并指定其作用域。如下面的代码。
@interface CRobot : NSObject
{
@private
int counter = 0;
@protected
int x;
int y;
@public
int identity;
}
-(void)move;
@end
其中,counter变量为私有实例变量,只能在本类中的实例方法中使用;x和y变量定义为受保护的实例变量,可以在本类或其子类中的实例方法中使用;而identity变量则定义为公共的,它可以由外部代码使用->运算符调用,如下面的代码。
CRobot *robot5 = [[CHello alloc] init];
robot5->identity = 5;
NSLog(@"当前ID : %i", robot5->identity);
不过,在类中定义公共变量并不是好的开发方法,如果我们需要这一类的数据形式,可以将这个数据定义为属性。
作用域
定义一个类时,有些成员是需要提供给外部代码调用的,而有些成员则只能在类的内部使用,此时,我们就应该考虑成员的作用域问题。
一般来讲,我们将公共成员(如属性和方法)声明在接口部分(必须在实现部分实现它们),而只限于本类或其子类使用的成员则可以直接定义在类的实现部分。
4.3. 方法
O博士:本节,我们就来讨论如何在Objective-C的类中实现方法(method)。再次说明,在Apple公司的文档中,方法称为任务(Task)。
4.3.1. 创建方法
前面,我们已经了解到,在类中的方法可以分为实例方法和类方法,在这里,我们将主要讨论实例方法,并根据参数的数量分为三种情况来介绍:
- 没有参数的方法。
- 有一个参数的方法。
- 有两个或更多参数的方法。
为什么要这样分,我想这一定某些人的情怀造成的,相信我,这绝对不是我的原因,我只是想,这样介绍也许更能帮助大家理解。不过,在这之前,我们需要了解,方法都是要设置返回值类型的,与函数不同,类中的方法,共返回值类型需要使用一对圆括号,如下面的代码:
-(int) getId;
如果返回值类型是一个对象类型,不要忘记使用*符号,对象就是指针!,如下面的代码:
-(NSString*) getName;
如果方法没有返回值,则使用void关键字来指定,如:
-(void) move;
接下来,我们再来讨论参数的三种情况。
没有参数的方法
前面,我们的示例中创建的就是没有参数的方法,其格式很简单,如(以实例方法为例):
-(<返回值类型>) <方法名>;
如下面的代码,我们就会在CRobot类中再定义一个没有参数的方法,首先在接口部分声明它。
@interface CRobot : NSObject
-(void) work;
// 其它代码
@end
然后,在实现部分定义这个方法。
@implementation CRobot
-(void) work
{
NSLog(@"机器人工作中");
}
// 其它代码
@end
在代码中,我们使用如下代码调用这个方法。
CRobot *robot5 = [[CRobot alloc] init];
[robot5 work];
有一个参数的方法
当方法有一个参数时,我们使用如下格式定义(以实例方法为例)。
-(<返回值类型>) <方法名> : (<参数类型>)<参数变量>;
我们还是先在接口部分声明方法,如下面的代码。
@interface CRobot : NSObject
-(void) work : (NSString*) name;
// 其它代码
@end
然后,在实现部分定义这个方法。
@implementation CRobot
-(void) work : (NSString*) name
{
NSLog(@"机器人 %@ 正在工作", name);
}
// 其它代码
@end
代码中,我们使用如下代码调用这个方法。
CRobot *robot5 = [[CRobot alloc] init];
[robot5 work : @"No.5"];
你也许发现了,我们定义的两个方法都是work,这不会有冲突吗?不会的,它们的参数数量是不一样的,调用时会自动匹配,找到需要执行的那一个方法。
有两个或更多参数的方法
当方法中有两个或更多的参数时,参数之间使用空格符分隔,从第二个参数开始使用下面的定义格式。
<参数名称>:(<参数类型>) <参数变量>
如下面的方法,我们将定义两个参数,首先还是在接口部分声明(以实例方法为例)。
@interface CRobot : NSObject
-(void) moveToX:(float)mx Y:(float) my;
@end
接着,我们在类的实现部分定义这个方法。
@implementation CRobot
-(void) moveToX:(float)mx Y:(float) my
{
NSLog(@"移动坐标到(%f, %f)", mx, my);
}
@end
在代码中,我们使用如下代码来调用这个方法。
CRobot *robot5 = [[CRobot alloc] init];
[robot5 moveToX:10.0 Y:15.0];
实际上,我们可以看到,这种定义参数的形式会让方法使用起来更像是一句自然语言,比如这个moveToX方法的调用,包含参数的意思就是“移动到X坐标10.0和Y坐标15.0”。不过,这里的自然语言显然指的是英文。^_^
当然,如果不想在方法中使用绕口的洋文,我也可以不使用<参数名称>,如下面的代码。
// 接口部分
@interface CRobot : NSObject
-(void) moveTo:(float)mx :(float)my;
@end
// 实现部分
@implementation CRobot
-(void) moveTo:(float)mx :(float)my
{
NSLog(@"移动坐标到(%f, %f)", mx, my);
}
@end
如果不使用<参数名称>,则每个参数使用冒号(:)开始,然后包括<参数类型>和<参数变量>即可。我们可以使用如下代码来调用这个方法。
CRobot *robot5 = [[CRobot alloc] init];
[robot5 moveTo:100 :200];
4.3.2. description方法与NSLog()函数
O博士:在Objective-C定义的类中,我们可以使用一些特殊成员,description方法就是其中之一,它的功能就是能够让对象以指定的形式显示文本信息,这样做的目的当然是让类或对象的信息更有意义。
如下面的代码,我们将在实现代码中重写这个方法。
@implementation CRobot
-(NSString*) description
{
return @"这是机器人对象";
}
@end
然后,我们可以通过在NSLog()函数中使用%@格式化字符来显示这个信息,如下面的代码。
CRobot *robot5 = [[CRobot alloc] init];
NSLog(@"%@", robot5);
此外,我们已经看到NSLog()函数中的一些常用格式化字符,包括第2章介绍的基本数据类型的格式化字符;%@格式化字符则用于显示任意类型的对象信息,而这些信息就可以使用类中的description方法来定义。
通过NSLog()和description方法的配合使用,还可以有效地帮助我们在开发对代码进行调试,如通过信息检测对象的结果是否是我们所期望的那样。
4.4. 属性
O博士:我们说过,在类中定义的属性是表示对象的特性,如名称、颜色、速度、尺寸、位置等等。在Objective-C中,常用的属性定义方式有两种,一种是使用@proeprty和@synthesize指令快速创建,称为存储属性(stored property);另一种是使用setter和getter方法创建,称为计算属性(compute property)。下面我们就分别介绍这两种创建属性的方式。
4.4.1. 使用@proeprty和@synthesize指令
在类中使用@proeprty和@synthesize指令定义属性,主要有两个步骤。
第一步,在类的接口部分使用@property指令声明属性的类型和名称,如下面的代码。
@interface CRobot : NSObject
@property NSString* name;
// 其它代码
@end
如果多个属性的类型是一样的,我们还可以使用一个@property指令同时声明多个属性,如:
@property float x, y;
在这个代码,我们同时定义了float类型的两个属性,即x和y。
接下来,我们需要做的就是在类的实现部分同步这些属性,如下面的代码:
@implementation CRobot
@synthesize name;
@synthesize x,y;
// 其它代码
@end
请注意,在类的实现部分,使用@synthesize指令同步属性时,不再需要指定属性的数据类型;这样一来,在@synthesize指令中,我们实际上可以将不同类型的属性写成一行代码,如:
@synthesize name, x, y;
应用中,我们可以通过圆点运算符(.)来使用对象的属性,如下面的代码。
CRobot *robot5 = [[CRobot alloc] init];
robot5.name = @"No.5";
robot5.x = 5.0;
robot5.y = 6.0;
NSLog(@"机器人%@的位置在(%f, %f)", robot5.name, robot5.x, robot5.y);
使用@property和@synthesize指令创建属性,的确非常方便,但也有一些不足;最明显的就是在设置属性值时,我们无法在类的内部对数据进行更多的操作,只能在设置属性值前对数据进行正确性检查等处理工作。如果我们需要在设置属性值时,在类的内部对数据进行更多的操作,可以使用getter和setter方法来创建类的属性。
4.4.2. 使用setter和getter方法
使用setter和getter方法创建类的属性时,我们一般会使用一个内部实例变量来保存属性的值,然后,我们会定义对应的方法来设置和读取这个变量的值,如下面的代码,我们首先在类的接口部分声明一个属性(speed)的设置和读取方法。
@interface CRobot : NSObject
-(float) speed;
-(void) setSpeed:(float)s;
// 其它代码
@end
接下来,我们会在类的实现部分定义属性数据变量,以及这几个方法的具体实现,如下面的代码。
@implementation CRobot
{
float _speed;
}
-(float) speed
{
return _speed;
}
-(void) setSpeed:(float)s
{
_speed = fabs(s);
}
// 其它代码
@end
在这个代码中,我们需要注意以下几个问题。
- 计算属性一般会在内部使用一个实例变量保存真正的数据,如代码中的_speed变量。
- getter方法用于获取属性值,其名称也就是属性的名称,而它的实现也相对简单,一般来讲,直接返回相应变量的值就可以了。
- setter方法用于设置属性值,其命名规则是“set+属性名”,其中,属性名首字母大写。对于本例中的setter方法实现,我们也使用了比较简单的处理方法;我们知道,物体的速度不能是负数,我们直接将传入属性值的绝对值赋值给了变量。这是特殊的处理方式,在实际开发工作中,我们可以根据需要对传入的数据进行检查和再加工。
使用getter和setter方法定义的属性,同样可以使用圆点(.)运算符,调用,如下面的代码。
CRobot *robot5 = [[CRobot alloc] init];
robot5.name = @"五号";
robot5.speed = 50.0;
NSLog(@"%@ 的速度是 %f km/h", robot5.name, robot5.speed);
此外,我们也可以看到,使用setter和getter方法定义的属性,其本质上还是方法,所以,我们也可以使用方法的形式来调用它们;但出于其它实际功能上的考虑,我们还是应该对属性和方法的应用加以区分。