第27条 使用“class-continuation分类”隐藏实现细节

2017/9/1 posted in  第四章 协议与分类

第一种用途

类中经常会包含一些无须对外公布的方法及实例变最。其实这些内容也可以对外公布, 并且写明其为私有,开发者不应依赖它们。但是OC的动态性,使得不可能实现真正的私有方法或私有实例变量。

但是我们最好还是只把确实需要对外公布的那部分内容公开。那么,这种不需对外公布但却应该具有的方法及实例变量应该怎么写呢?此时,这个特殊的“class-continuation分类”就派上用场了。

“class-continuation分类”和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。

例如:

//EOCPerson.m
@interface EOCPerson () {
    NSString * _anInstanceVariable;
}
// Method declarations here 
@end
@implementation EOCPerson {
    int _anotherInstanceVariable;
}
// Method implementations here 
@end

我们这样定义的目的是将这些方法或者实例变量隐藏起来,只供本类使用。即便在公共接口里将其标注为private,也还是会泄漏实现细节。

例如有个绝密的类,不想给其他人知道。 假设你所写的某个类拥有那个绝密类的实例,而这个实例变量又声明在公共接口里面:

#import <Foundation/Foundation.h>

@class EOCSuperSecretClass;

@interface EOCClass : NSObject {
@private
    EOCSuperSecretClass *_secretInstance;
@end

这样别人就会知道有一个叫EOCSuperSecretClass的类了。

所以我们通常应该这样:

// EOCClass.h
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject 
@end

// EOCClass .m 
#import "EOCClass.h"
#import "EOCSuperSecretClass.h"
@interface EOCClass ()  {
    EOCSuperSecretClass *_secretInstance;
@end

@implementation EOCClass
// Methods here
@end

第二种用途

编写Objective-C++代码时 “class-continuation分类”也很有用。Objective-C++OCC++的混合体,其代码可以用这两种语言来编写。由于兼容性原因,游戏后 端一般用C++来写。另外,有时候要使用的第三方库可能只有C++绑定,此时也必须使用 C++来编码。在这些情况下,使用"class-continuation分类"会很方便。假设某个类打算这样写:

#import <Foundation/Foundation.h> 
#include "SomeCppClass.h" 
@interface EOCClass : NSObject { 
@private
    SomeCppClass _cppClass;
@end

该类的实现文件可能叫做EOCClass.mm,其中.mm扩展名表示编译器应该将此文件按Objective-C++来编译,否则,就无法正确引人SomeCppClass.h了。然而请注意,名为SomeCppClass的这个C++类必须完全引入,因为编译器要完整地解析其定义方能得知_cppClass实例变量的大小。于是,只要是包含EOCClass.h的类,都必须编译为 Objective-C++才行,因为它们都引入了SomeCppClass类的头文件。这很快就会失控,最终 导致整个应用程序全部都要编译为ObjeCtive-C++。这样显然会增加编码的负担。

也许我们会想用前向声明来避免导入SomeCppClass.h,比如:

#import <Foundation/Foundation.h> 

class SomeCppClass;

@interface EOCClass : NSObject { 
@private
    SomeCppClass *_cppClass;
@end

现在实例变量必须是指针,若不是,则编译器无法得知其大小,从而会报错。但所有指针的大小确实都是固定的,于是编译器只需知道其所指的类型即可。

虽然我们这样做没有#include "SomeCppClass.h"但是我们前向声明该类时所用的class关键字还是C++下的关键字,所以仍然需要按照OC来编译才行。

我们这里的解决方法还是一样,既然变量是private的,我们还是可以将它在“class-continuation分类”声明,改写成:

// EOCClass. h

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject


// EOCClass.mm 
#import "EOCClass.h"
#include "SomeCppClass.h"

@interface EOCClass ()  {
    SomeCppClass _cppClass;
}
@end

@implementation EOCClass 
@end

改写后的EOCClass类,其头文件里就没有C++代码了,使用头文件的人甚至意识不到其底层实现代码中混有C++成分。某些系统库用到了这种模式,比如网页浏览器框架WebKit,其大部分代码都以C++编写,然而对外展示出来的却是一套整洁的Objective-C接口。CoreAnimation里面也用到了此模式,它的许多后端代码都用C++写成,但对外公布的却是一套纯Objective-C接口。

第三种用法

就是将public接口中声明为“只读”的 属性扩展为“可读写”,以便在类的内部设置其值。

例如:

// .h文件
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName; 
@property (nonatomic, copy, readonly) NSString *lastName;

-(id) initWithFirstName : (NSString*) firstName
                lastName: (NSString*) lastName;

@end

我们一般会在“class-continuaticm分类”中把这两个属性扩展为“可读写”:

@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

-(void)p_privateMethod;
@end

只需要用上面几行代码就行了。现在EOCPerson的实现代码可以随意调用“setFirstName:”“setLastName:”这两个设置方法,也可以用“点语法”来设置属性。这样做很有用,既能令外界无法修改对象,又能在其内部按照需要管理其数据。

只会在类的实现代码中用到的私有方法也可以声明在“class-continuation分类”中。这么做比较合适,因为它描述了那些只在类实现代码中才会使用的方法。上述的私有方法加上了p_前缀。

第四种用法

当我们想要把对象所遵守的协议视为私有,就可以在“class-continuation分类”中声明。例如:

#import "EOCPerson•h"
#import "EOCSecretDelegate.h"
@interface EOCPerson () <EOCSecretDelegate> 
@end
@implementation EOCPerson
    /*.....*/
@end

要点

  • 通过“class-continuation分类”向类中新增实例变量。

  • 如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”

  • 把私有方法的原型声明在“class-continuation分类”里面。

  • 若想使类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。