本文共 3777 字,大约阅读时间需要 12 分钟。
本节书摘来自华章出版社《编写高质量代码:改善Objective-C程序的61个建议》一 书中的第1章,第1.2节,作者:刘一道,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
在面向对象开发语言中,如C++、C#、Java等语言中,对于类的描述,通常划分为头文件和源文件。头文件用于描述类的声明和可公开部分,而源文件用于描述类的方法或函数的具体实现,这也体现了面向对象语言的“封闭性”和“高内聚低耦合”的特性。而对于基于面向对象而设计的Objective-C也不例外,类分为头文件(.h)和源文件(.m)。
在OOP编程中有两个技术用于描述类与类或对象与对象之间的关系:一个是继承;另一个是复合。在Objective-C中,当一个类需要引用另一个类,即建立复合关系时,需要在类的头文件(.h)中,通过“#import ”修饰符来建立被引用类的指针。例如Car.h:// Car.h#import@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#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@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#import "Tire.h"
上面的代码进行编译时会出现编译错误,如果使用@class在两个类的头文件中相互声明,则不会有编译错误出现。虽然使用@class不会出现编译错误,但还是尽量避免这种“类循环依赖”的出现,因为这样容易造成类之间“高耦合”现象的产生,给以后代码的维护和管理带来很大的麻烦。
“#import”并非一无是处。既然 “#import”与“@class”相比有很多不足,那么是否可以用“@class”来完全代替“#import”?不可以,在一个头文件(.h)中包含多个类的声明定义时,要与该头文件声明的多个类建立复合关系,比较好的方式是,采用关键字“#import”来建立复合关系。例如,下面是头文件PersonType.h的定义://PersonType.h#import// Person类的声明定义@interface Person:NSObject{ NSInteger *sexType;}@property (nonatomic copy) NSString *firstname;@property (nonatomic copy) NSString *lastname;- (void) setSexType: : (int) index;@end // man类的声明定义@interface man:Personend //woman类的声明定义@interface woman:Personend;
要与上面头文件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。不管采取哪种方式,降低类与类的耦合度,降低不同文件代码之间过度的黏合性是首要的目的。代码的依赖关系过于复杂则会失去代码的重用性,给维护代码带来很大的麻烦,同时,使编译的应用的稳定性和高效性也大打折扣。转载地址:http://hejka.baihongyu.com/