第6章 源文件组织
到目前为止,我们讨论过的所有项目都是把源代码统统放入main.m文件中。类的main()函数,@interface和@implementation部分都被塞入同一个文件里。这种结构对于小程序和简便应用来说没什么问题,但是并不适用于较大的项目。随着程序规模越来越大,文件内容会越来越多,查找信息也会越来越困难。
回想一下你的学生时代。你不会把所有的期末论文都放在同一个文件里而会把每篇论文都单独存档,并起一个易懂的文件名。
将程序拆分为多个小文件有助于更快地找到重要的代码,而且其他人在查看项目时也能有个大致的了解。另外将代码放入多个文件还可以更容易地将有趣的类代码发给朋友:只需打包其中几个文件即可,不用打包整个项目。本章将讨论把程序代码拆分到不同文件中的方法。
拆分接口和实现
前面已提到,Objective-C类的源代码分为两部分。一部分是接口,用来展示类的构造。接口包含了使用该类所需的所有信息。编译器将@interface部分编译后,你才能使用该类的对象,调用类方法,将对象复合到其他类中,以及创建子类。
源代码的另一个组成部分是实现。@implementation部分告诉Objective-C编译器如何让该类工作。这部分代码实现了接口所声明的方法。
在类的定义中,代码很自然地被拆分为接口和实现两个部分,所以类的代码通常分别放在两个文件中。一个文件存放接口部分的代码:类的@interface指令、公共struct定义、enum常量、#defines和extern全局变量等。由于Objective-C继承了C的特点,所以上述代码通常放在头文件中。头文件名称与类名相同,只是用.h做后缀。例如,Engine类的头文件会被命名为Engine.h,而Circle类的头文件名称是Circle.h。
所有的实现内容,如类的@implementation指令、全局变量的定义、私有struct等都被放在了与类同名但以.m为后缀的文件中(有时叫做.m文件)。上面两个类的实现文件将会被命名为Engine.m和Circle.m。
如果用.mm做文件扩展名,编译器就会认为你是用Objective-C++编写的代码,这样你就可以同时使用C++和Objective-C来编程了。
在Xcode中创建新文件
创建新类时,Xcode会自动生成.h和.m文件。在Xcode程序中选择File->New->New File后,会出现窗口,它列出了Xcode能够创建的文件类型。
选中Objective-C Class,然后点击Next按钮,会弹出另一个窗口要求你填写类的名称。
你还可以选择新创建的类的父类(默认是NSObject)。每个类都必须有一个父类(NSObject没有父类,它是所有类的父类)。你可以在下拉菜单中指定这个新类的父类,如果下拉菜单中没你想要的父类,也可以直接输入类的名称。
点击Next按钮之后,Xcode会询问你将文件存储在哪里。最好选择当前项目中其他文件所在的目录位置。
此外你还会看到苍有趣的东西。比方说,你可以选择将新文件放入哪个组(Group)。目前我们不会详细讨论Target这个概念,只是顺便提一下:复杂的项目可以拥有多个Target,它们源文件的配置各不相同,构建规则也不同。
新类创建完毕后,Xcode会在项目中添加相应的文件,并在项目窗口中展示出来。
Xcode中有一个与项目同名的群组,文件都放在群组内的文件夹中。(你可以在项目导航器中浏览项目文件的构造。)这些文件夹(Xcode中称作群组)能够帮你组织项目中的源文件。例如,你可以创建一个用来存放用户界面类的群组,再建一个用来存放数据处理类的群组,这样你的项目将更易于浏览。在设置群组时,Xcode并不会在硬盘上移动文件或者创建目录。群组关系仅仅是由Xcode负责管理的一项奇妙的功能。当然如果你愿意的话,可以设置群组指向文件系统中某个特定的目录,Xcode会帮你将新建的文件放入该目录。
拆分Car程序
首先要创建两个继承自NSObject的类:Tire和Engine。之后把原来写在main.m中有关于Tire和Engine类的声明和实现分别复制到对应的.h和.m文件中。这里贴出Tire类的代码,Engine类与之类似:
Tire.h
#import <Foundation/Foundation.h>

@interface Tire : NSObject

@end
Tire.m
#import "Tire.h"

@implementation Tire

- (NSString *)description{
    return (@"我是一个轮胎");
}//description

@end//Tire
这里比较有趣的代码就是Tire.m中的#import了,它不是导入Foundation.h,而是导入了Tire.h。其实这是标准的过程,在你以后所创建的项目里基本都会这么写(.m文件都要#import对应的.h文件)。编译器需要知道类里的实例变量配置,这样才能生成合适的代码,但是它并不知道与.m文件配套的头文件是谁。所以我们要导入.h文件。如果在程序编译过程中碰到了诸如“Cannot find interface declaration for Tire”(无法找到Tire类的接口定义)之类的错误信息,通常是因为你忘记用#import导入类的头文件了。




说明:注意,导入头文件有两种方法:使用引号或者尖括号。例如,#import<Cocoa/Cocoa.h>和#import “Tire.h”。带尖括号的语句用于导入系统头文件,而带引号的语句则说明导入的是项目本地的头文件。如果你看到的头文件名是用尖括号括起来的,那么这个头文件对你的项目来说是只读的,因为它属于系统。如果头文件名前后用的是引号,那么你便可以编辑它。
现在,重复上面的步骤来创建Slant6类和AllWeatherRadial类。此时程序可能会报告错误,这可能是由于Slant6类和AllWeatherRadial类没有导入Engine和Tire类的头文件造成的,自己手工补一下,将engine.h和tire.h文件#import到对应的子类头文件(.h文件)中即可。下面列出各类文件中导入的头文件情况:
Tire.h:
#import <Foundation/Foundation.h>
Tire.m:
#import "Tire.h"
Engine.h
#import <Foundation/Foundation.h>
Engine.m
#import "Engine.h"
Slant6.h
#import "Engine.h"
Slant6.m
#import "Slant6.h"
AllWeatherRadial.h
#import "Tire.h"
AllWeatherRadial.m
#import "AllWeatherRadial.h"
Car.h
#import "Engine.h"
#import "Tire.h"
Car.m
#import "Car.h"
main.m
#import <Foundation/Foundation.h>
#import "Car.h"
#import "Slant6.h"
#import "AllWeatherRadial.h"
使用跨文件依赖关系
依赖(dependency)是两个实体之间的一种关系。在编程和开发过程中,经常会出现关于依赖关系的问题。依赖关系可以存在于两个类之间,例如,Slant6类困继承关系而依赖于Engine类。如果Engine类发生了变化,例如添加了一个新的实例变量,那么就需要重新编译Slant6来适应这个变化。
依赖关系也可以存在于两个或多个文件之间。Car.h依赖于Tire.hEngine.h文件。如果两个文件中的任何一个发生了变化,都需要重新编译Car.m来适应这个变化。
导入头文件使头文件和源文件之间建立了一种紧密的依赖关系。如果头文件有任何变化,那么所有依赖它的文件都得重新编译。即使有一堆超性能主机任你使用,也需要花费相当长的时间。
由于依赖关系是传递的,头文件之间也可以互相依赖,所以重新编译的问题会更加严重。不过,尽管重新编译需要花费很长的时间,但至少Xcode能帮你记录所有的依赖关系。
重新编译须知
Objective-C提供了一种方法,能够减少由依赖关系引起的重新编译带来的负面影响。导致依赖关系问题的原因是Objective-C编译器需要某些信息才能够工作。但其实编译器有时只需要知道类名就可以了,不需要了解太多。
例如,在对象复合之后,复合通过指针指向对象。这之所以行得通,是因为所有Objective-C对象都使用动态分配的内存。编译器只需要知道这是一个类就可以了,然后就会知道实例变量的大小,就是一个指针的大小,无需知道更多。
前面我们使用#import的方式很好的完成了Car.h文件和Car.m文件,但它产生的重编译副作用也很明显,所以现在我可以采用另一种方式:@Class,这种方法足以告知编译器处理Car类的@interface部分所需要的全部信息了。不过这种方式在Car.m中是不行的,因为Car.m中我们需要创建具体Engine和Tire实例,所以编译器必须知道这两个类的全部内容。在Car.m中我们只能使用导入(#import)。
说明:@class创建了一个前向引用。这是在告诉编译器:“相信我,以后你自然会知道这个类到底是什么,但是现在,你知道这些足矣。”
如果有循环依赖关系,@class也很有用。即A类使用B类,B类也使用A类。如果试图通过#import语句让这两个类互相引用,那么就会出现编译错误。但是如果在A.h文件中使用@class B,在B.h中使用@class A,那么这两个类就可以互相引用了。
让汽车跑一会儿

Car.m需要更多关于Tire和Engine的信息,所以在Car.m中要导入Tire.h和Engine.h。(默然说话:我在这里出了个问题,因为在前面我并没有将Car.m中的init方法代码删除,所以即使在Car.m中导入了Tire.h和Engine.h,init方法仍然在报错,不知道是何原因。后来只好把init删除了。)修改后的Car.m的代码如下:
//
//  Car.m
//  CarParts
//
//  Created by mouyong on 13-7-6.
//  Copyright (c) 2013年 mouyong. All rights reserved.
//

#import "Car.h"
#import "Engine.h"
#import "Tire.h"

@implementation Car


- (void) print{
    NSLog(@"%@",engine);
    NSLog(@"%@",tires[0]);
    NSLog(@"%@",tires[1]);
    NSLog(@"%@",tires[2]);
    NSLog(@"%@",tires[3]);
}//print
-(Engine *)engine{
    return engine;
}//engine
-(void) setEngine:(Engine *)newEngine{
    engine=newEngine;
}//setEngine
-(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

@end//Car
导入和继承
Slant6和AllWeatherRadial继承自我们自己创建的类:Slant6继承自Engine,AllWeatherRadial继承自Tire。所以在头文件里不能使用@class。只能在Slant6.h中#import “Engine.h”,在AllWeatherRadial.h中#import “Tire.h”。
为什么呢?因为编译器需要先知道所有关于父类的详细信息才能成功地为子类编译@interface部分。它需要了解父类中实例变量的配置信息。
最后,main.h中只剩下了#import命令和一个孤零零的函数。如下所示:
//
//  main.m
//  CarParts
//  类与类的关系:复合
//  Created by mouyong on 13-6-23.
//  Copyright (c) 2013年 mouyong. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "Car.h"
#import "Slant6.h"
#import "AllWeatherRadial.h"


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
小结
在本章中,我们学习了使用多个文件来组织源代码的基本技巧。通常,每个类都有两个文件:包含类@interface部分的头文件和包含@implementation的.m文件。类的使用者可以通过#import命令导入头文件来获得该类的功能。在学习过程中,我们认识了文件的依赖关系,在这种关系中,头文件或源文件需要使用另一个头文件中的信息。文件导入过于混乱会延长编译时间,也会导致不必要的重复编译,而巧妙地使用@class指令告诉编译器“相信我,你最终肯定会了解这个名称的类”,可以减少必须导入的头文件的数量,从而缩短编译时间。(默然说话:另外,由于导入的延展性和Coaca的自动检查,我们可以每个头文件都只导入一遍,就算重复导入了,也不用太担心过份的占用内存,因为Coaca会帮我们把重复的导入进行合并,只是不知道这个机制靠不靠谱)
接下来我们将领略一些有趣的Xcode功能,下一章见。