[iOS]编写高质量代码-2

[iOS]编写高质量代码-2

与 C++ 及 C 语言相似,Objective-C 同样采用头文件(.h)与实现文件(.m)分离的方式来组织代码。在 Objective-C 中定义一个类时,通常会创建两个文件:头文件用于声明接口,实现文件用于具体实现。一个类的基本结构如下所示:

在 Objective-C 中,绝大多数类都需要引入 Foundation.h。除非当前类本身引入该文件,否则应引入其超类所属框架对应的基础头文件。举例来说,开发 iOS 应用时,我们常会继承 UIViewController,此时子类的头文件就需要引入 UIKit.h。

目前看来,EOCPerson 类并无不妥。它引入了整个 Foundation 框架,这本身没有问题。如果这个类继承自 Foundation 中的某个类,那么使用 EOCPerson 的开发者很可能会用到其基类中的大量功能。同样,继承自 UIViewController 的类也会引入 UIKit 中的大多数内容。

假设后来我们创建了一个名为 EOCEmployer 的新类,并希望每个 EOCPerson 实例都能关联一个 EOCEmployer 对象。我们很自然地会添加这样一个属性:

// EOCPerson.h
#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end

但这里存在一个问题:当其他文件引入 EOCPerson.h 时,编译器并不知道 EOCEmployer 类的存在。我们不应强制要求使用者在引入 EOCPerson.h 时必须同时引入 EOCEmployer.h,常见的做法是在 EOCPerson.h 中加入:

#import "EOCEmployer.h"

这种方法虽然可行,但不够理想。在编译使用 EOCPerson 的文件时,我们其实不需要了解 EOCEmployer 的全部细节,只需知道存在这个类即可。幸运的是,编译器提供了一种声明方式:

@class EOCEmployer;

这被称为“向前声明”。现在 EOCPerson 的头文件可以改为:

// EOCPerson.h
#import <Foundation/Foundation.h>

@class EOCEmployer;

@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end

而在 EOCPerson 的实现文件中,由于需要实际使用 EOCEmployer,这时才需要引入其头文件:

// EOCPerson.m
#import "EOCPerson.h"
#import "EOCEmployer.h"

@implementation EOCPerson
// 实现代码
@end

延迟引入头文件的时机,只在真正需要时才引入,可以有效减少使用者需要引入的头文件数量。假设我们在 EOCPerson.h 中直接引入 EOCEmployer.h,那么所有引入 EOCPerson.h 的文件都会自动引入 EOCEmployer.h 的全部内容。如果这种依赖链持续延伸,就会导致引入大量不需要的内容,从而增加编译时间。

向前声明还能解决两个类相互引用的问题。假设我们要为 EOCEmployer 类添加招聘和解雇员工的方法:

// EOCEmployer.h
#import "EOCPerson.h"

@interface EOCEmployer : NSObject
- (void)addEmployee:(EOCPerson *)person;
- (void)removeEmployee:(EOCPerson *)person;
@end

这时就产生了循环依赖:编译 EOCEmployer 需要知道 EOCPerson,而编译 EOCPerson 又需要知道 EOCEmployer。如果在各自头文件中相互引入对方的头文件,虽然使用 #import 不会导致无限循环,但会导致其中一个类无法正确编译。

然而在某些情况下,我们不得不在头文件中引入其他头文件。比如当继承某个父类时,必须引入定义该父类的头文件;同样,当声明遵循某个协议时,该协议必须有完整定义,不能使用向前声明。例如,创建一个继承自图形类并遵循绘制协议的矩形类:

// EOCRectangle.h
#import "EOCShape.h"
#import "EOCDrawable.h"

@interface EOCRectangle : EOCShape <EOCDrawable>
@property (nonatomic, assign) float width;
@property (nonatomic, assign) float height;
@end

这里的第二个 #import 是无法避免的。因此,最好将协议单独放在一个头文件中。如果将 EOCDrawable 协议放在一个庞大的头文件里,那么引入该协议就会连带引入大量不必要的内容,这既会产生依赖问题,也会拖慢编译速度。

不过有些协议,比如委托协议,就不适合单独放在一个头文件中。这类协议只有与使用它的类放在一起定义才具有实际意义。在这种情况下,最好在实现文件中通过 class-continuation 分类来声明对协议的遵循:

// EO***lass.m
#import "EO***lass.h"
#import "EOCDelegate.h"

@interface EO***lass () <EOCDelegate>
@end

@implementation EO***lass
// 实现代码
@end

这样只需要在实现文件中引入委托协议的头文件,而不必将其暴露在公共头文件中。

在决定是否在头文件中引入其他头文件时,务必谨慎考虑。如果可以用向前声明替代,就尽量不要引入。若因为定义属性、实例变量或遵循协议而必须引入头文件,应尽量将其移至 class-continuation 分类中。这样不仅能缩短编译时间,还能降低模块间的耦合度。过度复杂的依赖关系会给代码维护带来困难,特别是在需要将部分代码公开为 API 时,这种影响会更加明显。

核心要点

  • 除非必要,避免在头文件中引入其他头文件。一般而言,应在头文件中使用向前声明来引用其他类,并在实现文件中引入对应的头文件,以降低类之间的耦合度。

  • 当无法使用向前声明时(如声明遵循某个协议),应尽量将该声明移至 class-continuation 分类中。如果不可行,则考虑将协议单独放在一个头文件中再行引入。

持续为大家更新进阶书籍内容。

如需资料,可以在手机上打开地址下载。
资料书籍下载地址https://pan.xunlei.***/s/VOa09Mg2s6c-CAL6Fi-nuU0lA1?pwd=4wys#

转载请说明出处内容投诉
CSS教程网 » [iOS]编写高质量代码-2

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买