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。
![](https://app.yinxiang.com/shard/s11/res/866e8f0e-3724-4064-8b6b-4da9792bb641.png)
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
如果在类中没有包含任何实例变量,便可以省略接口定义中的花括号。
![](https://app.yinxiang.com/shard/s11/res/4b0b24a0-79ce-4384-ad35-31aeb17906e8.png)
自定义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
![](https://app.yinxiang.com/shard/s11/res/84893906-1a92-428d-81d8-c54fd54e09b7.png)
记住, %@只是调用每个对象的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方法为代码提供了通过对象自身访问对象属性的方式。
![](https://app.yinxiang.com/shard/s11/res/3a6be1c7-14e2-4b33-899f-8a2f13bd0822.png)
为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就违反了命名惯例。
![](https://app.yinxiang.com/shard/s11/res/3fd7d588-4b5b-425e-91b9-460230296f91.png)
设置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子类
![](https://app.yinxiang.com/shard/s11/res/ca3e3f66-29af-4f3b-a245-b6b32e9393ff.png)
Tire类
![](https://app.yinxiang.com/shard/s11/res/8c8c67a2-c7da-4a38-bd01-9ce90ab4a118.png)
main方法改为
![](https://app.yinxiang.com/shard/s11/res/381c4dca-43da-4056-945e-05fa459b8078.png)
这里我们添加了两个新类,当没有修改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文件)。
![](https://app.yinxiang.com/shard/s11/res/6fd7b5f9-dc7d-4b00-8d0f-a92eeb1e8510.png)
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
![](https://app.yinxiang.com/shard/s11/res/eb110c7e-820c-45bd-9cc5-03c5428d118d.png)
![](https://app.yinxiang.com/shard/s11/res/6b3646a1-290c-4d43-a7be-a9ae6f6328cf.png)
使用跨文件依赖关系
依赖关系(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部分所需的全部信息了。
![](https://app.yinxiang.com/shard/s11/res/d2474485-342a-4ae7-b3ca-b690a77110be.png)
我们还要在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
![](https://app.yinxiang.com/shard/s11/res/f620db80-d883-420b-8637-f9c312324d88.png)
![](https://app.yinxiang.com/shard/s11/res/3584e222-fe29-4e47-a318-a40b5a365864.png)
改变公司名称
在Xcode编译器导航器面板中选中项目,并确保在编译器面板的Project栏目下选中项目名称。右边的检查器面板选中项目,在Project Document栏目下修改Organization文本框。
![](https://app.yinxiang.com/shard/s11/res/e93c4c5f-d3b4-4352-b582-daf18beeed2c.png)
Xcode的代码自动完成(code completion), 出来的提示代码名称旁边的彩色方框表示这个符号的类型:E表示枚举符号,f表示函数,#表示#define指令,m表示方法,C表示类, 等等。
![](https://app.yinxiang.com/shard/s11/res/a85b7d6b-6ed1-4dea-9ce3-5cc53fc72640.png)
![](https://app.yinxiang.com/shard/s11/res/983cd11a-3de9-477c-a40e-0bc72408e517.png)
代码的导航
emace:
![](https://app.yinxiang.com/shard/s11/res/7f7e5e66-cf5c-45d1-81df-941957de022d.png)
![](https://app.yinxiang.com/shard/s11/res/4f573498-e4d3-4de3-b891-ea703cacc003.png)
![](https://app.yinxiang.com/shard/s11/res/bfe0f986-b180-45f9-8bd3-a633b725f284.png)
![](https://app.yinxiang.com/shard/s11/res/96aa361e-f931-4b8d-8339-e8cb54df4e49.png)
调试:
暴力测试(caveman debugging),在程序中写入输出语句(NSLog)来输出程序的控制流程和一些数据值。
![](https://app.yinxiang.com/shard/s11/res/ce6b4056-9fc5-475e-884c-9da734741a03.png)
在聚焦栏左边的宽条,可以看到一个蓝色箭头状的物体,那就是新断点,可以将断点拖出边栏删除。
![](https://app.yinxiang.com/shard/s11/res/60013599-780d-4677-b081-a4d13733a722.png)
![](https://app.yinxiang.com/shard/s11/res/e7724b68-aff7-4b35-8c1c-9d65a17c2993.png)
![](https://app.yinxiang.com/shard/s11/res/3cdf8aca-ae8c-4151-a243-93a37374972c.png)
![](https://app.yinxiang.com/shard/s11/res/7a8814d8-0073-4a36-a130-718091c6ba60.png)
![](https://app.yinxiang.com/shard/s11/res/92734254-374c-4a51-97c8-482195cfc6db.png)
![](https://app.yinxiang.com/shard/s11/res/fd94e5b9-02dc-47cf-830b-11e8920c6b01.png)
![](https://app.yinxiang.com/shard/s11/res/aa47fff1-4197-4d6c-b4fc-c7237770f7bc.png)