关键词:#import、#class、class-continuation 分类、点语法(直接访问还是通过属性访问)、协议等
文章是参考书籍与博客的总结,自己写下来也算是自我总结,加深印象~
一、前言
在 OOP 编程中有两个技术用于描述类或对象与对象之间的关系:一个是继承;另一个是复合。在 Object-C 中 ,当一个类需要引用另一个类,即建立复合关系时,需要在类的头文件 (h) 中,通过 #import 修饰符来建立被引用的指针。
通常情况下我们都使用 "#import" 修饰符引用类,但从代码质量与安全角度来看,使用 "#import" 建立复合关系时,也暴露了所引用类的实体变量与方法。很多时候我们并不需要知道关于这个类的更多信息,那么只需要了解它是通过指针引用即可,同时也减少了依赖关系,也减少了重新编译所产生的影响。
二者在编译效率上也存在很大差异。对于已经产生的引用,若被引用的头文件有变化时,那么引用它的类都需要重新编译,这将耗费大量的时间,而使用 @class 则不会。
对于初学者,使用 #import 还容易犯 "类循环依赖" 错误。即两个类互相引用对方,使用 @class 互相引用虽然不会出现编译错误,但还是尽量避免这种 "类循环依赖" ,因为容易造成高耦合多依赖,不便于维护。
二、何时使用 #import 何时使用 @class
既然 #import 有很多不足之处,但是很多情况下不得不用 #import,如在一个头文件 (.h) 中包含多个类的声明定义时,要与该头文件声明的多个类建立复合关系,即所引用的类所处的文件有多个类或者多个其他的定义,使用 #import 比较好。如下:
#import <Foundation/Foundation.h>
//B 类
@interface BClass : NSObject
@end
//C 类
@interface CClass : NSObject
@end
//D 类
@interface DClass : NSObject
@end
一般来说,使用 @class 只是为了在头文件中引用这个类,把之当做类型来用。同时,在实现类 .m 中 ,如果需要引用这个类的实体变量或方法等,还需要通过 #import 把在 @class 中声明的类引用进来。下面举例说明一下:
#import <Foundation/Foundation.h>
@interface ClassA : NSObject
@property (nonatomic, copy) NSString *title;
@end
@import UIKit.UIViewController;
@class ClassA;
@interface ViewController : UIViewController
@property (nonatomic, strong) ClassA *aClass;
@end
#import "ViewController.h"
#import "ClassA.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSLog(@"%@", self.aClass.title);
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (ClassA *)aClass {
if (!_aClass) {
_aClass = [[ClassA alloc] init];
}
return _aClass;
}
@end
四、class-continuation 分类
综上,在设计类的时候,要分清何时使用 #import 何时使用 @class,是否有必要引入头文件等。若因为要实现属性、实例变量或者要遵循协议而必须引入头文件,则应尽量将其移至“class-continuation 分类”中,这样不仅可以缩减编译时间,而且还能降低彼此依赖程度。下面就讲讲 “class-continuation 分类”。
类中经常会包含一些无需对外公布的方法以及实例变量(当然也可以对外公布,并写明其为私有)。然后,我们最好还是只把确实需要对外公布的部分公开。那么不公开的怎么写呢,其实就是用“class-continuation 分类”。
“class-continuation 分类” 和普通分类不同,它必须定义在其类的实现文件中,与其他分类不同,“class-continuation 分类”没有名字。比如有个类叫 KPerson,其“class-continuation 分类” 写法如下:
@interface KPerson()
@end
看到这里,可能突然明白,哦,原来这就是“class-continuation 分类”~
我们平时也一直这么用...
@interface ViewController ()
@end
在分类其中可以定义方法和实例变量,放在里面的相当于隐藏起来,仅供本类使用。
当然的确可以写在头文件中,哪怕是将其标注为 private,也还是会泄露实现细节。别人知道有个视图宽度的属性了。或者你可以将这个值改为强类型 id,但是 id 类型不够友好,在实现类中无法获得编译器的帮助,即辅助检查功能,且自己也难以理解。
#import <Foundation/Foundation.h>
@interface ClassA : NSObject{
@private
NSInteger viewWidth;
}
@end
编写 C++ 代码时“class-continuation 分类” 尤为有用。由于游戏大多用 C++ 来写,若把 C++ 文件的引用放在头文件中(实现文件必须是 .mm 扩展名表示编译器应该将此文件按 Object-C++ 编译)。后果就是所有引入了这个引入了 C++ 文件的类,都要改为 .mm,这可能导致整个应用程序都改为 .mm 了(互相引用是有可能的)。使用“class-continuation 分类”即将 C++ 的引入放到了其实现文件中,头文件没有 C++ 代码了,使用这个头文件的人甚至不知道其内部混有 C++ 代码,对外暴露的是一套纯 Object-C 接口。
“class-continuation 分类”还有一种合理用法,就是将 public 接口中标为只读的属性扩展成可读写,以便在内部设置其值。通过触发键值观测通知,其他对象有可能正监听此事件。只需要像下面的几行代码就行。实现类既可以随意调用 setFirstName: 或 setLastName: 这两个设置方法,也可以用点语法来设置属性,这样做既可以让外界无法修改对象,又能在其内部按照需要管理其数据。
.h 文件
#import <Foundation/Foundation.h>
@interface ClassA : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@end
.m文件
#import "ClassA.h"
@interface ClassA()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation ClassA
@end
五、关于降低类与类的耦合度
在我看来,提高效率倒是其次,毕竟硬件发展这么快,这种优化提升不大(话说回来,精益求精还是需要的)。其实不管采取哪种方式都可以,首要目的还是为了降低耦合度,降低文件代码之间的粘合性,依赖关系过于复杂会失去代码重用性,给维护增加难度。
如果建立的复合关系过于复杂时(无论使用 #import 或 @class),这时可以考虑 2 种方式来解决:
网友评论