Objective-C 02 复合

复合:
 
编程中的复合(composition)就好像音乐中的作曲(composition)一样:将多个组件组合在一起,配合使用,从而得到完整的作品。 
在Objective-C中,复合是通过包含作为实例变量的对象指针实现的。
 
在软件开发中,程序员可能会使用一个Pedal(脚踏板)对象和一个Tire(轮胎)对象组合出虚拟的Unicycle(独轮车)。
虚拟的独轮车应该拥有一个指向Pedal对象的指针和一个指向Tire对象的指针。Code 如下:
@interface Unicycle : NSObject
{
     Pedal *pedal;
     Tire *tire;
}
@end // Unicycle
 
Pedal和Tire通过复合的方式组成了Unicycle。
 
Car程序
Code:
 
#import <Foundation/Foundation.h>
 
@interface Tire : NSObject
@end // Tire
 
@implementation Tire
-(NSString *) description {
     return (@"I am a tire. I last a while.");
} // description
@end // Tire
如果在类中没有包含任何实例变量,便可以省略接口定义中的花括号。
自定义NSLog()
 
NSLog() 可以使用%@格式说明符来输出对象。NSLog()处理%@说明符时,会询问参数列表中相应的对象以得到这个对象的描述。从技术上来讲,也就是 NSLog()给这个对象发送了description消息,然后对象的description方法生成一个NSString并将其返回.NSLog() 就会在输出结果中包含这个字符串。在类中提供description方法就可以自定义NSLog()会如何输出对象。
 
在Cocoa中,NSArray类管理的是对象的集合,它的description方法提供了数组自身的信息。
 
Engine类的Code
@interface Engine : NSObject
@end // Engine
 
@implementation Engine
- (NSString *) description
{
     return (@"I am an engine. Vrooom!");
} // description
 
@end // Engine
 
Car的Code
@interface Car : NSObject
{
     Engine *engine;
     Tire *tires[4];
}
-(void) print;
@end // Car
 
因 为engine和tires是Car的实例变量,所以它们是复合的。每一个Car对象都会指向engine和tires的指针分配内存。但是真正包含在 Car中的并不是engine和tires变量,只是内存中存在的其他对象的引用指针。为新建的Car对象分配内存时,这些指针将被初始化为nil(零 值),也即是这辆汽车现在既没有发动机也没有轮胎,可以想象成还在流水线上组装的汽车框架。
 
Car类的实现
首 先是一个初始化实例变量的init方法。该方法为汽车创建了用来装配的一个engine变量和4个tire变量。使用new创建新对象时,系统其实在后台 执行了两个步骤:第一步,为对象分配内存,即对象获得一个用来存放实例变量的内存块;第二步,自动调用init方法,使该对象进入可用状态。
@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
记住, %@只是调用每个对象的description方法并显示结果。
main()函数:
int main (int argc, const char *argv[])
{
     Car *car;
     car = [Car new];
     [car print];
     return (0);
} // main
 
存取方法
存取(accessor)方法是用来读取或改变某个对象属性的方法。如之前的setFillColor就是一个存取方法。因为它为对象中的变量赋值,所以这类存取方法被称为setter方法。(mutator用来更改对象状态)
 
另一种存取方法是getter方法。getter方法为代码提供了通过对象自身访问对象属性的方式。
为Car添加setter和getter方法,Code如下:
@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
 
存取方法总是成对出现的,一个用来设置属性的值(如setTire),一个用来读取属性的值(tireAtIndex);
但是有时只有一个setter或getter也是合理的。
 
对于存取方法的命名,Cocoa有自己的惯例。
setter方法根据它所更改的属性的名称来命名,并加上前缀set。如:setEngine,setStringValue。
getter方法则是以其返回的属性名称命名。所以上面的setter方法所对应的getter方法应该是engine,stringValue。不要将get用作getter方法的前缀。如getStringValue就违反了命名惯例。
设置engine属性的存取方法
两个方法的实现代码
-(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:方法后,依然只存在一个发动机,而不是两个。
 
设置tires属性的存取方法
Code:
-(void) setTire: (Tire *) tire atIndex: (int) index
{
     if (index < 0 || index > 3) {
          NSLog(@"bad index (%d) in setTire:atIndex: ", index);
          exit(1);
     }
     tires[index] = tire;
} // setTire:atIndex:
 
-(Tire *) tireAtIndex: (int) index
{
     if (index < 0 || index > 3) {
          NSLog(@"bad index(%d) in tireAtIndex:", index);
          exit(1);
     }
     return (tires[index]);
} // tireAtIndex:
tire存取方法中使用了通用代码来检查tires实例变量的数组索引,以保证它是有效数值。该代码就是所谓的防御式编程(defensive programming).
Tire *tire = [Tire new];
[car setTire: tire atIndex: 2];
NSLog(@"tire number two is %@", [car tireAtIndex: 2]);
 
main函数改为:
int main (int argc, const char *argv[])
{
     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
扩展CarParts程序
这时候我们就可以添加新的类了。
如:新的Engine子类
Tire类
 
main方法改为
 
这里我们添加了两个新类,当没有修改car类。
 
什么时候用继承,什么时候用复合
继承的类之间建立的关系为“is a” (是一个)。 如果可以说“X是一个Y”,那就可以使用继承。
复合的类之间建立的关系为“has a” (有一个)。 如果可以说“X有一个Y”, 那就可以使用复合。
创建新对象时,先花时间想清楚什么时候应该用继承,什么时候应该用复合。
 
复合是OOP的基础概念,通过这种技巧来创建引用其他对象的对象。
 
源文件组织
拆分接口和实现
Objective-C类的源代码分为两部分。一部分是接口,用来展示类的结构。接口包含了使用该类所需的所有信息.编译器将@interface部分编译后,才能使用该类的对象,调用类方法, 将对象复合到其他类中,以及创建子类。
 
源代码的另一个组成部分是实现。@implementation部分告诉Objective-C编译器如何让类工作,这部分代码实现了接口所声明的方法。
所以类的代码通常分别放在两个文件里。一个文件存放接口部分的代码:类的@interface指令、公共struct定义、enum常量、#defines和extern全局变量等。由于Objective-C继承了C的特点,所以上述代码通常放在头文件中。头文件名称与类名相同,只是用.h做后缀。
所有的实现内容(如类的@implementation指令、全局变量的定义、私有struct等)都被放在了与类同名但以.m为后缀的文件中(有时叫做.m文件)。
 
Code:
Tire.h
#import <Foundation/Foundation.h>
@interface Tire : NSObject
@end
 
Tire.m
 
#import "Tire.h" // 导入@interface的头文件,将此信息告知编译器,这样才能生成合适的代码。
 
@implementation Tire
-(NSString *) description
{
     return (@"I am a tire. I last a while");
} // description
 
@end // Tire
 
 
 
使用跨文件依赖关系
依赖关系(dependency)是两个实体之间的一种关系。依赖关系也可以存在于两个或多个文件之间。
导入头文件是头文件和源文件之间建立一种紧密的依赖关系。如果头文件有任何变化,那么所有依赖它的文件都得重新编译。如果编写了100个.m文件并导入同一个文件all.h里,一旦修改all.h,那么所有的100个.m都会重新生成,这个就非常耗费时间。
不仅如此,由于依赖关系是传递的,头文件之间也可以互相依赖,所以重新编译的问题会更加严重。
 
重新编译须知:
Objective-C提供了一种方法,能够减少由依赖关系引起的重新编译带来的负面影响。
导致依赖关系问题的原因是Objective-C编译器需要某些信息才能够工作。
Objective-C引入了关键字@class来告诉编译器:这是一个类,所以只会通过指针来引用它。这样编译器就不必知道关于这个类的更多信息了,只要了解它是通过指针来引用的即可。
 
在Car.h中,
#import <Cocoa/Cocoa.h>
 
@interface Car : NSObject
-(void) setEngine : (Engine *) newEngine;
-(Engine *) engine;
 
-(void) setTire: (Tire *) tire atIndex: (int) index;
-(Tire *) tireAtIndex: (int) index;
 
-(void) print;
 
如果想要使用这个头文件,会报错,错误信息可能是:error:expected a type "Tire",我们有两种方法来解决这个错误问题。第一种是用#import语句导入Tire.h和Engine.h,这样编译器会获得关于这两个类的许多信息。
 
还有一个更好的方法。如果仔细观察Car类的接口,你会发现它只是通过指针引用了Tire和Engine。这是@class可以完成的,下面是加入了@class代码的Car.h文件内容。
 
#import <Cocoa/Cocoa.h>
 
@class Tire;
@class Engine;
 
@interface Car : NSObject
-(void) setEngine : (Engine *) newEngine;
-(Engine *) engine;
 
-(void) setTire: (Tire *) tire atIndex: (int) index;
-(Tire *) tireAtIndex: (int) index;
 
-(void) print;
这样就足以告知编译器处理Car类的@interface部分所需的全部信息了。
 
 
我们还要在Car.m文件中导入Tire.h和Engine.h,还用把@implementation部分黏贴到Car.m文件中,这样才可以跑起来Car。
 
导入和继承
我们在原先没分开的Car类文件中创建一个类Slant6,它继承Engine类。由于它继承自其他类而不是通过指针指向其他类的,所以不能在头文件中使用@class语句。我们只能在Slant6.h文件中使用#import “Engine.h”语句。
 
那 么,为什么不能再这里使用@class语句呢?因为编译器需要先知道所有关于超类的信息才能成功为其子类编译@interface部分。它需要了解超类中 实例变量的配置信息(数据类型、大小和排序)。在子类添加实例变量时,它们会被附加在超类实例变量的后面。然后编译器就利用这些信息计算在内存的什么位置 能找到这些实例变量,每个方法都通过自身的self隐藏指针进行寻找。为了能够准确地计算出实例变量的位置,编译器必须先了解该类的所有内容。
 
然后在Xcode中创建文件Slant6.h和Slant6.m。
Slant6.h
#import "Engine.h"
 
@interface Slant6 : Engine
 
@end // Slant6
 
这个文件没有导入<Cocoa/Cocoa.h>,是因为Engine.h中已经导入了,所以不需要再导入一遍,但是如果加上也是可以的,#import不会重复导入已导入的文件
实现的代码:
 
#import "Slant6.h"
 
@implementation Slant6
 
-(NSString *) description
{
     return (@"I am a slant-6. VROOOm!");
}// description
 
@end // Slant6
 
深入了解Xcode
 
改变公司名称
在Xcode编译器导航器面板中选中项目,并确保在编译器面板的Project栏目下选中项目名称。右边的检查器面板选中项目,在Project Document栏目下修改Organization文本框。
Xcode的代码自动完成(code completion), 出来的提示代码名称旁边的彩色方框表示这个符号的类型:E表示枚举符号,f表示函数,#表示#define指令,m表示方法,C表示类, 等等。
 
代码的导航
emace:
 
调试
 
暴力测试(caveman debugging),在程序中写入输出语句(NSLog)来输出程序的控制流程和一些数据值。
 
 
在聚焦栏左边的宽条,可以看到一个蓝色箭头状的物体,那就是新断点,可以将断点拖出边栏删除。
 
 
posted @ 2015-11-30 16:27  三恒一书  阅读(186)  评论(0编辑  收藏  举报