第2条:在类的头文件中尽量少引入其他头文件

  本条要点:(作者总结)

  • 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。
  • 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation 分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。

 

  与 C 和 C++ 一样,Objective-C  也使用“头文件”(header file)与“实现文件”(implementation file )来区隔代码。用 Objective-C 语言编写“类”(class)的标准方式为:以类名做文件名,分别创建两个文件,头文件后缀用 .h,实现文件后缀用 .m。创建好一个类之后,其代码看上去是这样的:

 1 // EOCPerson.h
 2 #import <Foundation/Foundation.h>
 3 
 4 @interface EOCPerson : NSObject
 5 
 6 @property (nonatomic, copy) NSString *firstName;
 7 @property (nonatomic, copy) NSString *lastName;
 8 
 9 @end
10 
11 // EOCPerson.m
12 #import "EOCPerson.h"
13 
14 @implementation EOCPerson
15 // Implementation of methods
16 @end

  用 Objective-C 语言编写任何类几乎都要引入 Foundation.h。如果不在该类本身引入这个文件的话,那么就要引入与其超类所属框架相对应的“基本头文件”(base header file)。例如创建 UIViewController 子类,这些子类的头文件需要引入 UIKit.h。

  现在看来,EOCPerson 类还好,其头文件引入了整个 Foundation 框架,不过这并没有问题。如果此类继承自 Foundation 框架中的某个类,那么 EOCPerson 类的使用者(consumer)可能会用到其基类的很多内容。继承自 UIViewController 的那些类也是如此,其使用者可能会用到 UIKit 中的大部分内容。

  过段时间,可能又创建了一个名为 EOCEmployer 的新类,然后可能觉得每个 EOCPerson 实例都应该有一个 EOCEmployer。于是,直接为其添加一项属性:

 1 // EOCPerson.h
 2 #import <Foundation/Foundation.h>
 3 
 4 @interface EOCPerson : NSObject
 5 
 6 @property (nonatomic, copy) NSString *firstName;
 7 @property (nonatomic, copy) NSString *lastName;
 8 @property (nonatomic, strong) EOCEmployer *employer;
 9 
10 @end

  然而这么做有个问题,就是在编译引入了 EOCPerson.h 的文件时,EOCEmployer 类并不可见。不便强迫开发者在引入 EOCPerson.h 时必须一并引入 EOCEmployer.h,所以,常见的方法是在 EOCPerson.h 中加入这行:

1 #import "EOCEmployer.h"

  这种方法可行,但是不够优雅。在编译一个使用了 EOCPerson 类的文件时,不需要知道 EOCEmployer 类的全部细节,只需要知道有一个类名叫 EOCEmployer 就好。所幸有个方法能把这一情况告诉编译器:

1 @class EOCEmployer;

这叫做“向前声明”(forward declaring)该类。现在 EOCPerson 的头文件变成了这个样子:

 1 // EOCPerson.h
 2 #import <Foundation/Foundation.h>
 3 //#import "EOCEmployer.h"
 4 
 5 @class EOCEmployer;
 6 
 7 @interface EOCPerson : NSObject
 8 
 9 @property (nonatomic, copy) NSString *firstName;
10 @property (nonatomic, copy) NSString *lastName;
11 @property (nonatomic, strong) EOCEmployer *employer;
12 
13 @end

  EOCPerson 类的实现文件则需要引入 EOCEmployer 类的头文件,因为若要使用后者,则必须知道其所有接口细节。于是,实现文件就是:

1 // EOCPerson.m
2 #import "EOCPerson.h"
3 #import "EOCEmployer.h"
4 
5 @implementation EOCPerson
6 // Implementation of methods
7 @end

  将引入头文件的时机尽量延后,只在确有需要时才引入,这样就减少类的使用者所需引入的头文件数量。假设本例把 EOCEmployer.h 引入到 EOCPerson.h,那么只要引入 EOCPerson.h,就会一并引入 EOCEmployer.h 的所有内容。此过程若持续下去,则要引入许多根本用不到的内容,这当然会增加编译时间。

  向前声明也解决了两个类相互引用的问题。假设要为 EOCEmployer 类加入新增及删除雇员的方法,那么其头文件中会加入下述定义:

1 - (void)addEmployee:(EOCPerson *)person;
2 - (void)removeEmployee:(EOCPerson *)person;

  此时若要编译 EOCEmployer,则编译器必须知道 EOCPerson 这个类,而要编译 EOCPerson,则又必须知道 EOCEmployer。如果在各自头文件中引入对方的头文件,则会导致“循环引用”(chicken-and-egg situation)。当解析其中一个头文件时,编译器会发现它引入了另一个头文件,而那个头文件又回过头来引用第一个头文件。使用 #import 而非 #include 指令虽然不会导致死循环,但这却意味着两个类里有一个无法被正确编译。

  但是有时候必须要在头文件中引入其它头文件。如果写的类继承自某个超类,则必须引入定义那个超类的头文件。同理,如果要声明你写的类遵从某个协议(protocol),那么该协议必须有完整定义,且不能使用向前声明。向前声明只能告诉编译器有某个协议,而此时编译器却要知道该协议中定义的方法。

  例如,要从图形类中继承一个矩形类,且令其遵循绘制协议:

 1 // EOCRectangle.h
 2 #import "EOCShape.h"
 3 #import "EOCDrawable.h"
 4 
 5 @interface EOCRectangle : EOCShape<EOCDrawable>
 6 
 7 @property (nonatomic, assign) float width;
 8 @property (nonatomic, assign) float height;
 9 
10 @end

  第二条 #import 是难免的。鉴于此,最好是把协议单独放在一个头文件中。要是把 EOCDrawable 协议放在了某个大的头文件中,那么只要引入此协议,就必定会引入那个头文件中的全部内容,如此一来,就像上面说的那样,会产生相互依赖问题,而且还会增加编译时间。

  然而有些协议,例如“委托协议”(delegate protocol),就不用单独写一个头文件了。在那种情况下,协议只有与接受协议委托的类放在一起定义才有意义。此时最好能在实现文件中声明此类实现了该委托协议,并把这段实现代码放在“class-continuation 分类”(class-continuation category)里。这样的话,只要在实现文件中引入包含委托协议的头文件即可,而不需要将其放在公共头文件(public header file)里。

  每次在头文件中引入其他头文件之前,都要先问问自己这样做是否是有必要的。如果可以用向前声明取代引入,那么就不要引入。若因为要实现属性、实例变量或者遵循协议而必要引入头文件,则应尽量将其移至“class-continuation 分类”中。这样做不仅可以缩短编译时间,而且还能降低彼此依赖程度。若是依赖关系过于复杂,则会给维护带来麻烦,而且,如果只想把代码的某个部分开放为公共 API 的话,太复杂的依赖关系也会出问题。

 

END

posted @ 2017-06-11 08:30  鳄鱼不怕牙医不怕  阅读(221)  评论(0编辑  收藏  举报