现在的位置: 首页 > 综合 > 正文

Objective-C编程基础读书笔记(5)

2013年08月09日 ⁄ 综合 ⁄ 共 8769字 ⁄ 字号 评论关闭
  1. 第5章  复合

使用复合可以组合多个对象,让它们分工合作。在实际的程序中,你会同时用到继承和复合来创建自己的类,所以掌握这两个概念非常重要。

  1. 什么是复合

复合就是一个类持有另一个类的引用(默然说话:大多数面向对象理论书籍里的术语叫关联)。在Objective-C中,复合是通过包含作为实例变量的对象指针实现的。例如我们可以用一个Pedal(脚踏板)对象和一个Tire(轮胎)对象组合出虚拟的Unicycle(独轮车)。

@interface Unicycle:NSObject

{

    Pedal *pedal;

    Tire *tire;

}

@end //Unicycle

Car程序

现在准备搭建一个汽车模型。好吧,我们不会去费力地研究真正的汽车的物理模型,我们象这样考虑:1辆汽车只包含1台发动机和4个轮胎。轮胎和发动机都是仅包含一个方法的类,这个方法仅输出它们各自所代表的含义:轮胎对象会说它们是轮胎,发动机对象会说它是一台发动机。

CarParts程序的所有代码也是包含在main.m中。完整代码如下:

//

//  main.m

//  CarParts

//  类与类的关系:复合

//  Created by mouyong on 13-6-23.

//  Copyright (c) 2013年 mouyong. All rights reserved.

//

#import <Foundation/Foundation.h>

@interface Tire : NSObject

@end //Tire

@implementation Tire

- (NSString *)description{

    return (@"我是一个轮胎");

}//description

@end//Tire

@interface Engine : NSObject

@end//Engine

@implementation Engine

- (NSString *)description{

    return @"我是一个发动机";

}//description

@end//Engine

@interface Car : NSObject

{

    Engine *engine;

    Tire *tires[4];

}

-(void)print;

@end//Car

@implementation Car

- (id) init{

    if (self=[super init]) {

        engine=[Engine new];

        tires[0]=[Tire new];

        tires[1]=[Tire new];

        tires[2]=[Tire new];

        tires[3]=[Tire new];

    }

    return self;

}//init

- (void) print{

    NSLog(@"%@",engine);

    NSLog(@"%@",tires[0]);

    NSLog(@"%@",tires[1]);

    NSLog(@"%@",tires[2]);

    NSLog(@"%@",tires[3]);

}//print

@end//Car

int main(int argc, const char * argv[])

{

    Car *car;

    car=[Car new];

    [car print];

    return 0;

}//main

在Tire类中只有一个description方法,比较奇怪的是,它并没有在接口中声明。它是从哪儿来的?如果接口中并没有包含它,计算机又怎么能知道可以在Tire类里调用description方法呢?(默然说话:感觉description就象Java中的toString()

  1. 为SNLog定义输出字符串

NSLog()可以使用%@格式说明符来输出对象。NSLog()处理%@说明符时,会调用对象的description方法,然后对象的description方法生成一个NSString并将其返回。NSLog()就会在输出结果中包含这个字符串。在类中提供description方法就可以自定义NSLog()会如何输出对象。

在自定义的description方法中,既可以写一个很简单的字符串,也可以使用各种形式连接一个复杂的字符串。

Engine类也只有一个description方法。

最后一部分是Car本身,它拥有一个engine对象和一个由4个tire对象组成的数组。它通过复合的方式来组装自己。Car同时还有一个print()方法,该方法输出轮胎和发动机的描述。

因为engine和tires是Car类的实例变量,所以它们是复合的。你可以说汽车有4个轮胎和1个发动机。

@interface Car : NSObject

{

    Engine *engine;

    Tire *tires[4];

}

-(void)print;

@end//Car

每一个Car对象都会为指向engine和tires的指针分配内存,但是真正包含在Car中的并不是engine和tires变量,只是内存中存在的其他对象的引用指针。为新建的Car对象分配内存时,这些指针将被初始化为nil(默然说话:相当于Java中的null值。),也就是说这辆汽车现在既没有发动机也没有轮胎,但预留的安装发动机和轮胎的位置,只是还没装上去。

下面让我们在看看Car类的实现。首先是一个初始化实例变量的init方法。(默然说话:注意到声明中并没有声明init吧?直觉告诉我,这个方法应该是Java中的构造方法吧)该方法为我们的汽车创建了用来装配的1个engine和4个tire变量。使用new创建新对象时,系统其实在后台执行了两个步骤:第一步,为对象分配内存,即对象获得一个用来存放实例变量的内存块;第二步,自动调用init方法,使该对象进入可用状态。

Car类的init方法创建了4个新轮胎并赋值给tires数组,还创建了一台发动机并赋值给engine实例变量。

在init方法中比较奇怪的是那个if语句,我们解释一下这行代码的意思。为了让父类将所有需要的初始化工作一次性完成,你需要调用[super init]。意思是让父类初始化过程中返回的对象与一开始创建的保持一致。而将初始化返回的结果赋给self是Objective-C的惯例。

@implementation Car

- (id) init{

    if (self=[super init]) {

        engine=[Engine new];

        tires[0]=[Tire new];

        tires[1]=[Tire new];

        tires[2]=[Tire new];

        tires[3]=[Tire new];

    }

    return self;

}//init

- (void) print{

    NSLog(@"%@",engine);

    NSLog(@"%@",tires[0]);

    NSLog(@"%@",tires[1]);

    NSLog(@"%@",tires[2]);

    NSLog(@"%@",tires[3]);

}//print

@end//Car

最后一部分是main()函数,也是程序的驱动力。main()函数创建了一辆新车,并告诉它输出自身的信息,然后退出程序。

生成并运行CarParts程序,你应该会看到与下面内容类似的输出:

-------------------------------------------------------------------------------------------------------------------

2013-06-27 10:01:18.312 CarParts[16183:303]我是一个发动机

2013-06-27 10:01:18.314 CarParts[16183:303]我是一个轮胎

2013-06-27 10:01:18.315 CarParts[16183:303]我是一个轮胎

2013-06-27 10:01:18.315 CarParts[16183:303]我是一个轮胎

2013-06-27 10:01:18.316 CarParts[16183:303]我是一个轮胎

-------------------------------------------------------------------------------------------------------------------

  1. 存取方法

我们可以使用存取方法来改进CarParts。

经验丰富的编程人员看到Car类的init方法可能会问:“为什么汽车要自己创建轮胎和发动机呢?”如果用户能为汽车定做不同类型的轮胎和发动机,那么这个程序就会更完善了。

我们可以添加存取方法来实现上述想法。存取(accessor)方法是用来读取或改变某个对象属性的方法。例如前面Shapes-Object中的setFillColor:就是一个存取方法。如果添加一个新方法去改变Car对象中的engine对象变量,那它就是一个存取方法。因为它为对象中的变量赋值,所以这类存取方法被称为setter方法。你也许听说过mutator方法,它是用来更改对象状态的。

另一种存取方法当然是getter方法。getter方法为代码提供了通过对象自身访问对象属性的方式。在赛车游戏中,物理逻辑引擎可能会想要读取汽车轮胎的属性,以此来判断赛车以当前的速度行驶是否会在湿滑的道路上打滑。

说明:如果要对其他对象中的属性进行操作,应该尽量使用对象提供的存取方法,绝对不能直接改变对象里面的值。例如,main()函数不应直接访问Car类的engine实例变量(通过car->engine的方法)来改变engine的属性,而应使用setter方法进行更改。

存储方法是程序间接工作的另一个例子。使用存取方法间接地访问car对象中engine,可以让car的实现更为灵活。

下面为Car添加一些setter和getter方法,这样它就有选用轮胎和发动机的自主权了。下面是Car类的新接口。

@interface Car : NSObject

{

    Engine *engine;

    Tire *tires[4];

}

-(Engine *)engine;

-(void)setEngine:(Engine *)newEngine;

-(Tire *)tireAtIndex:(int) index;

-(void)setTire:(Tire *) tire atIndex:(int)index;

-(void)print;

@end//Car

代码中的实例变量并没有变化,但是新增了两对方法:engine和setEngine:用来处理发动机的属性,tireAtIndex和setTire:atIndex:用来处理轮胎的属性。存取方法总是成对出现的,一个用来设置属性的值,另现代战争用来读取属性的值。有时只有一个getter方法(用于只读属性)或者只有一个setter方法(只写属性)也是合理的。但通常情况下,我们都会同时编写setter和getter方法。

对于存取方法的全名,Cocoa有自己的惯例。

setter方法根据它所更改的属性的名称来命名,并加上前缀set。

getter方法则是以其返回的属性名称命名,不要将get用作getter方法的前缀。

说明 get这个词在Cocoa中有着特殊的含义。如果get出现在Cocoa的方法名称中,就意味着这个方法会将你传递的参数作为指针来返回数值。

如果你在存取方法的名称中使用了get,那么有经验的Cocoa编程人员就会习惯性地将指针当做参数传入这个方法,当他们发现这不过是一个简单的存取方法时就会感到困惑。最好不要让其他编程人员被你的代码搞得一头雾水。(默然说话:作为一个有经验的Java编程人员,在看到getter方法不写get前缀时我感到非常困惑,我现在已经一头雾水啦~~

  1. 设置engine属性的存取方法

第一对存取方法用来访问发动机的属性:

-(Engine *)engine;

-(void)setEngine:(Engine *)newEngine; 

在代码中调用Car对象的engine方法可以访问engine变量,调用setEngine:方法可以更改发动机的属性。下面是实现代码:

-(Engine *)engine{

    return engine;

}//engine

-(void) setEngine:(Engine *)newEngine{

    engine=newEngine;

}//setEngine

getter方法engine返回实例变量engine的当前值。记住,在Objective-C中所有对象间的交互都是通过指针实现的,所以方法engine返回的是一个指针,指向Car中的发动机对象。

同样,setter方法setEngine:将实例变量engine的值赋为参数所指向的值。实际上被复制的并不是engine变量,而是指向engine的指针值。换一种方式说,就是在调用了对象Car中的setEngine:方法后,依然只存在一个发动机,而不是两个。

说明   为了信息的完整性,我们需要说明,在内存管理和对象所有权方面,Engine的getter方法和setter方法还存在着问题。但是现在就把内存管理和对象生命周期管理的问题摆出来,肯定会让你困惑和沮丧,所以我们把如何准确无误的编写存取方法放到第8章再讲。

  1. 设置tires属性的存取方法

tires的存取方法稍微复杂一点:

-(Tire *)tireAtIndex:(int) index;

-(void)setTire:(Tire *) tire atIndex:(int)index;

由于汽车的4个轮胎都有自己不同的位置(汽车车体的4个角落各有一个轮胎),所以Car对象中包含一个轮胎的数组。在这里我们需要用索引存取器而不能直接访问tires数组。所以在为汽车配置轮胎时,不仅需要知道是哪个轮胎,还要清楚每个轮胎在汽车上的位置。同样,当访问汽车上的某个轮胎时,访问的也是这个轮胎的具体位置。

下面是相关存取方法的实现代码:

-(Tire *)tireAtIndex:(int)index{

    if (index <0 || index>3) {

        NSLog(@"错啦 索引值%d不对!",index);

        exit(1);

    }

    return (tires[index]);

}//tireAtIndex

-(void)setTire:(Tire *)tire atIndex:(int)index{

    if (index <0 || index>3) {

        NSLog(@"错啦 索引值%d不对!",index);

        exit(1);

    }

    tires[index]=tire;

}//setTire:atIndex

tire存取方法检查了tires实例变量的数组索引,以保证它是有效的。如果传入的索引值不在0到3的范围,程序将会输出错误信息并退出。这就是所谓防御式编程(defensive programming),这是种很好的编程思想。防御式编程能够在开发早期发现错误,比如tires数组的索引错误。

  1. Car类代码的其他变化

首先Car类的init方法。由于Car现在已经有访问engine和tires变量的方法,所以init方法就不需要再创建这两个变量了,这样一来,init方法都可以去除(默然说话:当然,你要保留它也行,这样程序就多一种选择,既可以弄到一辆已经装配好的车,又可以随时更换轮胎和发动机),因为已经不需要在Car中做这些工作了。新车的车主会得到一辆没有轮胎和发动机的汽车,可以自己装配它。

接着我们可以更新一下main()函数,如下:

int main(int argc, const char * argv[])

{

    Car *car;

    car=[Car new];

    Engine *engine=[Engine new];

    [car setEngine:engine];

    for (int i=0; i<4; i++) {

        Tire *tire=[Tire new];

        [car setTire:tire atIndex:i];

    }

    [car print];

    return 0;

}//main

从用户的角度来看,程序的运行结果并没有任何改变。

  1. 扩展CarParts程序

既然Car类已经有了存取方法,就应该充分利用它。我们不会照搬当前的发动机和轮胎,而是对这些部件做一些改变。用集成方式来创建新的发动机和轮胎,然后使用Car类的存取方法(复合方式)给汽车配置新的部件。

首先创建一个新型的发动机Slant6。

@interface Slant6 : Engine

@end//Slant6

@implementation Slant6

-(NSString *)description{

    return (@"我是一个Slant6发动机!");

}//description

@end//Slant6

Slant6是发动机,所以它是Engine的子类。要记住,继承可以让我们在需要父类的的地方使用子类。在Car类中,setEngine:方法需要的是Engine型的参数,所以我们可以放心的传递Slant6型的参数。

在Slant6类中,description方法被重写,用来输出新信息。由于Slant6并没有调用父类中的description方法,所以新信息完全替代了继承自Engine的描述信息。

轮胎的新类AllWeatherRadial的实现步骤与创建Slant6的步骤非常相似。将其定义为现有类Tire的子类,然后重写description方法。

@interface AllWeatherRadial : Tire

@end//AllWeatherRadial

@implementation AllWeatherRadial

-(NSString *)description{

    return @"我是一个全天候轮胎";

}//description

最后调整main()函数,使用痛苦的型的发动机和轮胎

int main(int argc, const char * argv[])

{

    Car *car;

    car=[Car new];

    Engine *engine=[Slant6 new];

    [car setEngine:engine];

    for (int i=0; i<4; i++) {

        Tire *tire=[AllWeatherRadial new];

        [car setTire:tire atIndex:i];

    }

    [car print];

    return 0;

}//main

main()仅仅是重新new了两个新类,其他部分并无改动,然后我们就发现输出已经完全变了。

----------------------------------------------------------------------------------------------------------------------

2013-07-06 10:16:38.476 CarParts[576:303] 我是一个Slant6发动机!

2013-07-06 10:16:38.478 CarParts[576:303] 我是一个全天候轮胎

2013-07-06 10:16:38.478 CarParts[576:303] 我是一个全天候轮胎

2013-07-06 10:16:38.479 CarParts[576:303] 我是一个全天候轮胎

2013-07-06 10:16:38.479 CarParts[576:303] 我是一个全天候轮胎

----------------------------------------------------------------------------------------------------------------------

  1. 复合还是继承

CarParts同时用到了继承和复合,也就是我们在前一章和本章中所介绍的两个“万能”工具。那么,什么时候用继承,什么时候用复合?

继承的类之间建立的关系为“是”的关系,三角形是形状,Slant6是发动机,AllWeatherRadial是轮胎。所以,如果可以说“X是Y”,那么就可以使用继承。

复合的类之间建立的关系为“有”。形状有颜色,汽车有发动机和轮胎。与继承的区别:汽车不是发动机,也不是轮胎。所以,如果可以说“X有Y”,那就可以使用复合。

新手在进行面向对象编程时通常会犯这样的错误:对任何东西都想使用继承,例如让Car类继承Engine类。这样的确可以让代码正常运行,但是其他人阅读这段代码时会不理解:汽车是发动机?!当然不是。所以只应在适当的时机使用继承。

  1. 小结

复合是OOP的基础概念,我们通过这种技巧来创建引用其他对象的对象。例如,汽车对象引用了1个发动机和4个轮胎对象。在本章关于复合的讨论中,我们介绍了存取方法,它既为外部对象提供了改变其属性的途径,同时又能保护实例变量本身。

存取方法和复合是密不可分的,因为我们通常都会为复合的对象编写存取方法。我们还学习了两种类型的存取方法:setter方法和getter方法,前者告诉对象将属性改为什么,后者要求对象提供属性的值。

本章还介绍了Cocoa存取方法的命名规则。需要特别指出的是,对于返回属性值的存取方法,名称中不能使用get这个词。

下一章我们不会谈论任何OOP理论,而是介绍如何分割不同的类,并将它们放入多个源文件中,而不是把所有代码都写到一个大文件里。



本人接受个人捐助,如果你觉得文章对你有用,并愿意对默然说话进行捐助,数额随意,默然说话将不胜感激。

抱歉!评论已关闭.