欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

#import @class 的区别

程序员文章站 2024-01-15 08:19:28
...

关键词:#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 种方式来解决:

1.模块方式(这个我也不是很明白,有待了解)
2.协议:后续文章讲这个