本节书摘来自华章出版社《编写高质量代码:改善Objective-C程序的61个建议》一 书中的第1章,第1.2节,作者:刘一道,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
建议2:在头文件中尽量减少其他头文件的引用
在面向对象开发语言中,如C++、C#、Java等语言中,对于类的描述,通常划分为头文件和源文件。头文件用于描述类的声明和可公开部分,而源文件用于描述类的方法或函数的具体实现,这也体现了面向对象语言的“封闭性”和“高内聚低耦合”的特性。而对于基于面向对象而设计的Objective-C也不例外,类分为头文件(.h)和源文件(.m)。
在OOP编程中有两个技术用于描述类与类或对象与对象之间的关系:一个是继承;另一个是复合。在Objective-C中,当一个类需要引用另一个类,即建立复合关系时,需要在类的头文件(.h)中,通过“#import ”修饰符来建立被引用类的指针。例如Car.h:
// Car.h
#import <Foundation/Foundation.h>
@interface Car:NSObject
{
Tire *tires[4];
Engine *engine;
}
- (void) setEngine: (Engine *) newEngine;
- (Engine *) engine;
- (void) setTire: (Tire *) tire atIndex: (int) index;
- (Tire *) tireAtIndex: (int) index;
- (void) print;
@end // Car
在这里先省略类Car的源文件(.m)。对于上面的代码,如果直接这么编译,编译器会报错,提示它不知道Tire和Engine是什么。为了使上面的代码能编译通过,在上面代码中就不得不添加对类Tire和Engine的头文件(.h)的引用,即通过关键字“#import”来建立起它们之间的复合关系。
要建立正确的复合关系,正确的代码写法如下:
// Car.h
#import <Foundation/Foundation.h>
#import "Tire.h"
#import "Engine.h"
@interface Car:NSObject
{
Tire *tires[4];
Engine *engine;
}
- (void) setEngine: (Engine *) newEngine;
- (Engine *) engine;
- (void) setTire: (Tire *) tire atIndex: (int) index;
- (Tire *) tireAtIndex: (int) index;
- (void) print;
@end // Car
现在,上面的代码虽然能正确编译了,但从“代码的高品质高安全”角度来看,在使用“#import”建立类之间的复合关系时,也暴露了所引用类Tire和Engine的实体变量和方法,与只需知道有一个类名叫Tire和Engine的初衷有些违背。在解决问题的同时,也带来了代码的安全性问题。
那么如何解决上面的问题呢?可以使用关键字@class来告诉编译器:这是一个类,所以只需要通过指针来引用它。它并不需要知道关于这个类的更多信息,只要了解它是通过指针引用即可,减少由依赖关系引起的重新编译所产生的影响。
对于上面的代码,通过@Class即可来建立对于类Tire和Engine的引用,具体写法如下:
// Car.h
#import <Foundation/Foundation.h>
@class Tire
@class Engine
@interface Car:NSObject
{
Tire *tires[4];
Engine *engine;
}
- (void) setEngine: (Engine *) newEngine;
- (Engine *) engine;
- (void) setTire: (Tire *) tire atIndex: (int) index;
- (Tire *) tireAtIndex: (int) index;
- (void) print;
@end // Car
上面介绍了使用“#import”和“@class”在“依赖关系”方面所产生的影响。同时二者在编译效率方面也存在巨大的差异。假如,有100个头文件,都用“#import”引用了同一个头文件,或者这些文件是依次引用的,如A–>B、B–>C、C–>D这样的引用关系。当最开始的那个头文件有变化时,后面所有引用它的类都需要重新编译,如果自己的类有很多的话,这将耗费大量的时间,而使用@class则不会。
对于初学者,最容易犯 “类循环依赖”错误。所谓的“类循环依赖”,也就是说,两个类相互引用对方。在本条款最初的Car.h头文件中,通过“#import ”引用了Tire.h头文件,假如在Tire.h头文件里引用Car.h头文件,即如下:
// Tire.h
#import <Foundation/Foundation.h>
#import "Tire.h"
上面的代码进行编译时会出现编译错误,如果使用@class在两个类的头文件中相互声明,则不会有编译错误出现。虽然使用@class不会出现编译错误,但还是尽量避免这种“类循环依赖”的出现,因为这样容易造成类之间“高耦合”现象的产生,给以后代码的维护和管理带来很大的麻烦。
“#import”并非一无是处。既然 “#import”与“@class”相比有很多不足,那么是否可以用“@class”来完全代替“#import”?不可以,在一个头文件(.h)中包含多个类的声明定义时,要与该头文件声明的多个类建立复合关系,比较好的方式是,采用关键字“#import”来建立复合关系。
例如,下面是头文件PersonType.h的定义:
//PersonType.h
#import <Foundation/Foundation.h>
// Person类的声明定义
@interface Person:NSObject
{
NSInteger *sexType;
}
@property (nonatomic copy) NSString *firstname;
@property (nonatomic copy) NSString *lastname;
- (void) setSexType: : (int) index;
@end
// man类的声明定义
@interface man:Person
end
//woman类的声明定义
@interface woman:Person
end;
要与上面头文件PersonType.h中所声明的类建立复合关系,这个时候就不得不用关键字“#import”。使用“#import”建立复合关系,会把所引用的头文件(.h)的所有类进行预编译,这样就会消耗很长时间。是否是有一种更好的方式来处理这个问题,请参阅“条款 9 尽量使用模块方式与多类建立复合关系”。
一般来说,关键字“@class”放在头文件中只是为了在头文件中引用这个类,把这个类作为一个类型来用。这就要求引用的头文件(.h)名与类的名称一致,且在类头文件(.h)只包含该类的声明定义的情况下,才可以使用关键字“@class”来建立复合关系。同时,在实现这个类的接口的类源文件(.m)中,如果需要引用这个类的实体变量或方法等,还需要通过“#import”把在“@class”中声明的类引用进来。例如下面的类a引用类Rectangle的示例:
a.h
@class Rectangle;
@interface A : NSObject {
...
}
a.m
#import Rectangle
@implementation A
...
上面的种种介绍,其核心的目的就是为了“降低类与类之间的耦合度”。也就是说,降低类与类之间的复合关系黏性度。
在自己设计类的时候,除了“#import”和“@class”之外,有没有一种更好的方式?有的,一种是通过使用模块方式与多类建立复合关系,详细情况请参阅建议6;另一种是通过使用“协议”的方式来实现。
在Objective-C中,实际上,协议就是一个穿了“马甲”的接口。通过使用“协议”来降低类与类之间的耦合度。例如,把协议单独写在一个文件中,注意,千万不要把协议写入到一个大的头文件中,这样做,凡是只要引入此协议,就必定会引入头文件中的全部内容,如此一来,类与类之间的耦合度就会大大增加,不利于代码的管理及程序的稳定性和安全性,为以后的工作带来很大的麻烦。
故此,在自己设计类的时候,首先要明白,通过使用“#import”和“@class”,每次引入其他的头文件是否有必要。如果要引用的类和该类所在的文件同名,最好采用“@class”方式来引入。如果引用的类所处的文件有多个类或者多个其他的定义,最好采用“模块方式”来针对性引入自己所需要的类,详细情况请参与建议6。不管采取哪种方式,降低类与类的耦合度,降低不同文件代码之间过度的黏合性是首要的目的。代码的依赖关系过于复杂则会失去代码的重用性,给维护代码带来很大的麻烦,同时,使编译的应用的稳定性和高效性也大打折扣。