协议与分类

协议

OC语言中有一项特性叫做“协议”,与Java中的“接口”类似。OC不支持多重继承,因而我们把某个类应该实现的一系列方法定义在协议里面。协议最为常见的用途是实现委托模式,当然也有其他用法

通过委托与数据源协议进行对象间通信

对象之间经常需要互相通信,而通信方式有多种。OC开发者广泛使用一种名叫“委托模式”的编程设计模式来实现对象间的通信。该模式的主旨是:定义一套接口,某对象若想接受另一对象的委托,则需要遵从此接口,以便成为其“委托对象”。而这另一对象则可以回传一些信息,也可以在发生相关事件时通知委托对象。之前我在iOS跨界面传值中有简述过协议的基本使用
比方说,用户界面里面有个显示一系列数据所用的视图,那么,此视图只应包含显示数据所需的逻辑代码,而不应决定要显示何种数据以及数据之间如何交互等问题。视图对象的属性中,可以包含负责数据与事件处理的对象。这两种对象分别称为“数据源”与“委托”
在OC中,一般通过“协议”这项语言特性来实现此模式,整个Cocoa系统框架都是这么做的。
为演示此模式,我们举个例子,假设要编写一个从网上获取数据的类。此类也许要从远程服务器的某个资源里获取数据。那个远程服务器可能要过很长时间才会应答,而在获取数据的过程中阻塞应用程序会比较糟糕。于是,在这种情况下,我们通常会使用委托模式:获取网络数据的类含有一个“委托对象”,在获取完数据之后,它会回调这个委托对象。DataModel请求NetworkFetcher“以异步方式执行一项任务”,而NetworkFetcher在执行完这项任务之后就会通知其委托对象,即DataModel
在这里插入图片描述
利用协议机制,很容易就能以OC代码实现此模式,上述例子可以这么定义:

@protocol NetworkFetcherDelegate <NSObject>

- (void)networkFetcher:(NetworkFetcher *)fetcher didReceiveData: (NSData *)data;
- (void)nerworkFetcher:(NetworkFetcher *)fetcher didFailWithError: (NSError *)error;

@end

委托协议名通常是在相关类名后加delegate,整个类名采用驼峰法。
有了这个协议以后,类就可以用一个属性来存放其委托对象了。在本例中,这个类就是NetworkFetcher类。于是,此类的接口可以写成这样:

@interface NetworkFetcher : NSObject

@property (nonatomic, weak) id <NetworkFetcherDelegate> delegate;

@end

注意:这个属性需要定义成weak而不是strong,因为两者之间必须为“非拥有关系”。通常情况下,扮演delegate的那个对象也要持有本对象。例如在本例中,想使用NetworkFetcher的那个对象(即DataModel)就会持有本对象,直到用完本对象后才会释放。假如声明属性的时候用strong将本对象与委托对象之间定为“拥有关系”,那么就会引入“保留环”。因此,存放委托对象的这个属性要么定义成weak,要么定义成unsafe_unretained,前者可以在相关对象销毁时自动清空,后者不可。
在这里插入图片描述
实现委托对象的办法是声明某个类遵从委托协议,然后把协议中想实现的那些方法在类里实现出来。某类若要遵从委托协议,可以在其接口中声明,也可以在“class-continuation分类”声明。如果要向外界公布此类实现了某协议,那么就采用前者,而如果这个协议是个委托协议的话,那么通常只会在类的内部使用。所以说,这种情况一般都是在“class-continuation分类”中声明:

@interface DataModel () <NetworkFetcherDelegate>

@end

@implementation DataModel

- (void)networkFetcher:(NetworkFetcher *)fetcher didReceiveData: (NSData *)data {
    // do something
}

- (void)nerworkFetcher:(NetworkFetcher *)fetcher didFailWithError: (NSError *)error {
    // do something
}

@end

委托协议中的方法一般都是可选的,因为受委托者这个对象未必关心其中的所有方法。在本例中,DataModel类可能并不关心获取数据的过程中是否有错误,所以可以不实现“nerworkFetcher: didFailWithError: ”这个方法。为了指明可选方法,委托协议经常使用@optional 关键字来标注其大部分或全部的方法

@protocol NetworkFetcherDelegate <NSObject>
@optional 
- (void)networkFetcher:(NetworkFetcher *)fetcher didReceiveData: (NSData *)data;
- (void)nerworkFetcher:(NetworkFetcher *)fetcher didFailWithError: (NSError *)error;

@end

如果要在委托对象上调用可选方法,那么必须提前使用类型信息查询方法判断这个委托对象是否能够响应相关选择子。以NetworkFetcher为例:

NSError *error = /* error obtained from network */
if ([_delegate respondsToSelector: @selector(nerworkFetcher:didFailWithError:)]) {
	[_delegate nerworkFetcher:self didFailWithError:error];
}

这段代码用“respondsToSelector:”来判断委托对象是否实现了相关方法。如果实现了就调用;如果没实现就不执行。这样的话,delegate对象就可以完全按照其需要来实现委托协议中的方法了,不用担心因为某个方法没实现而导致程序崩溃。
上一段代码中,在调用delegate对象中的方法时,总是应该把发起委托的实例也一并传入方法中,这样,delegate对象在实现相关方法时,就能根据传入的实力分别执行不同的方法了,例如:

- (void)networkFetcher:(NetworkFetcher *)fetcher didReceiveData: (NSData *)data {
    if (fetcher == _myFetcherA) {
    	//do something
    } else if (fetcher == _myFetcherB){
    	//do another thing
    }
}

delegate里的方法也可以用于从获取委托对象中获取信息。比方说,NetworkFetcher类也许想提供一种机制:在获取数据时如果遇到了“重定向”,那么将询问其委托对象是否应该发生重定向。delegate对象中的相关方法可以写成这样:

- (BOOL)networkFetcher:(NetworkFetcher *)fetcher shouldFollowRedirectToURL:(NSURL *) url;

通过这个例子,现在应该理解此模式为何叫做“委托模式”了,因为对象把应对某个行为的责任委托给另外一个类了。
也可以用协议定义一套接口,令某类经由该接口获取其所需的数据。委托模式的这一用法旨在向类提供数据,故而又称“数据源模式”。在此模式中,信息从数据源流向类;而在常规的委托模式中,信息则从类流向受委托者。
在这里插入图片描述
比方说,用户界面框架中的“列表视图”对象可能会通过数据源协议来获取要在列表中显示的数据。除了数据源之外,列表视图还有一个受委托者,用于处理用户与列表的交互操作。将数据源协议与委托协议分离,能使接口更加清晰,因为这两部分的逻辑代码也分开了。另外,“数据源”与“受委托者”可以是两个不同的对象。然而一般情况下,都用同一个对象来扮演者两种角色。
在实现委托模式与数据源模式时,如果协议中的方法时可选的,那么就会写出一大批类似下面的代码来:

扫描二维码关注公众号,回复: 11490253 查看本文章
if ([_delegate respondsToSelector: @selector(someClassDidSomething:)]) {
	[_delegate someClassDidSomething:];
}

很容易用代码查出某个委托对象是否能响应选择子,可是如果频繁执行此操作,除了第一次检测的结果有用之外,后续的检测都是多余的。如果委托对象本身没变,那么不太可能会突然可以响应之前不能响应的选择子。鉴于此,我们通常把委托对象能否响应某个协议方法这一信息缓存起来,可以优化效率。假设在上例中,delegate对象所遵从的协议里有个表示数据获取进度的回调方法,每当数据获取有进度时,委托对象就会得到通知。即:

@protocol NetworkFetcherDelegate <NSObject>
@optional 
- (void)networkFetcher:(NetworkFetcher *)fetcher didReceiveData: (NSData *)data;
- (void)nerworkFetcher:(NetworkFetcher *)fetcher didFailWithError: (NSError *)error;
- (void)nerworkFetcher:(NetworkFetcher *)fetcher didUpdataProgressTo:(float)progress;
@end

增加了“nerworkFetcher: didUpdataProgressTo:”方法。将方法响应能力缓存起来的最佳途径时使用“位段”数据类型。
可以在该实例中嵌入一个含有位段的结构体作为其实例变量,而结构体中的每个位段则表示delegate对象是否实现了协议中的相关方法,即:

@interface NetworkFetcher () {
	struct {
		unsigned int didReceiveData       :1;
		unsigned int didFailWithError     :1;
		unsigned int didUpdataProgressTo  :1;
	} _delegateFlags;
}
@end

我使用了“class-contin分类”来增加实例变量,而新增的这个实例变量是个结构体,有三个位段,每个位段都与delegate所遵从的协议中某个可选方法相对应。在FetworkNetcher类中可以这么使用:

// set flag
_delegateFlages.didReceiveData = 1;
//check flag
if (_delegateFlags.didReceiveData) {
	//do something
}

这个结构体用来缓存委托对象是否响应特定的选择子。实现缓存功能所用的代码可以写在delegate属性所对应的设置方法里:

- (void)setDelegate:(id<NetworkFetcher>)delegate {
	_delegate = delegate;
	_delegateFlags.didReceiveData = [delegate respondsToSelector: @selector(nerworkFetcher: didReceiveData:)];
	_delegateFlags.didFailWithError = [delegate respondsToSelector: @selector(nerworkFetcher: didFailWithError:)];
	_delegateFlags.didUpdateProgressTo = [delegate respondsToSelector: @selector(nerworkFetcher: didUpdateProgressTo:)];
}

这样的话,每次调用delegate的相关方法之前,就不用检测委托对象是否能够响应选择子了

分类

类中经常容易填满各种代码,而这些方法的代码则全部堆在一个巨大的实现文件里。有时这么做是合理的,因为即便通过重构把这个类打散,效果也不会更好。在此情况下,可以使用OC的“分类”机制,把类代码按逻辑划入几个分区中,这对开发与调试都有好处。

将类的实现代码分散到便于管理的数个分类之中

比如,我们把个人信息建模为类,那么这个类就可能包含下面几个方法:

@interface Person : NSObject

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

- (id)initWithfirstName:(NSString *)firstName andLastName:(NSString *)lastName;

/* friend methods */
- (void)addFriends:(Person *)person;
- (void)removeFriends:(Person *)person;
- (BOOL)isFriendsWith:(Person *)person;

/* work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;

/* play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;

@end

实现该类时,所有的代码会在一个大文件里。如果还向类中继续添加方法的话,那么源代码文件就会越来越大,难以管理。我们可以用“分类”机制改写:

@interface Person : NSObject

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

- (id)initWithfirstName:(NSString *)firstName andLastName:(NSString *)lastName;

@end

@interface Person (Friendship)

- (void)addFriends:(Person *)person;
- (void)removeFriends:(Person *)person;
- (BOOL)isFriendsWith:(Person *)person;

@end

@interface Person (Work)

- (void)performDaysWork;
- (void)takeVacationFromWork;

@end

@interface Person (Play)

- (void)goToTheCinema;
- (void)goToSportsGame;

@end

现在,类的实现代码被分成了好几块。在本例中,类的基本要素(诸如属性与初始化方法等)都声明在“主实现”里。执行不同类型的操作所用的另外几套方法则归入各个分类中。
使用分类机制后,依然可以把整个类都定义在一个接口文件中,并将其代码写在一个实现文件里。可是,随着分类数量增加,当前这份实现文件很快就能膨胀的无法管理了。此时可以把每个分类提去到各自文件中去。上例可以拆分成:

  • Person+Friendship (.h/.m)
  • Person+Work (.h/.m)
  • Person+Play (.h/.m)

具体的创建分类这篇博客中讲过

比方说,与交友功能相关的那个分类可以这么写:

//Person+Friendship.h
#import "Person.h"

@interface Person (Friendship)

- (void)addFriends:(Person *)person;
- (void)removeFriends:(Person *)person;
- (BOOL)isFriendsWith:(Person *)person;

@end

//Person+Friendship.m
#import "Person+Friendship.h"
@implementation Person (Friendship)

- (void)addFriends:(Person *)person {
    //do something
}

- (void)removeFriends:(Person *)person {
    //do something
}

- (BOOL)isFriendsWith:(Person *)person {
    //do something
}

@end

通过分类机制可以把类代码分成很多小块,以便单独检视。使用分类机制后,如果想用分类中的方法,那么要记得在引入Person.h时一并引入分类的头文件。虽然稍微有点麻烦,不过分类仍是一种管理代码的好办法。

之所以要将类代码打散到分类中还有个原因就是便于调试:对于某个分类中的所有方法来说,分类名称都会出现在其符号中。例如:“addFriend:”方法的“符号名”如下:

- [Person(Friendship) addFriend:]

在调试器的回溯消息中,会看到类似下面这样的内容:

frame #2: 0x00001c50 Test '-[EOCPerson(Friendship) addFriend:] + 32 at main.m : 46

根据回溯信息中的分类名称,很容易就能精确定位到类中的方法所属的功能区,这对于某些应该视为私有的方法来说更是极为有用。可以创建名为Private的分类,把这种方法全都放在里面。这个分类里的方法一般只会在类或框架内部使用,而无须对外公布。这样一来,类的使用者有时可能会在查看回溯信息时发现private一词,从而知道不应该直接调用此方法了。这可算作一种编写"自我描述式代码"的办法。
在编写准备分享给其他开发者使用的程序库时,可以考虑创建Private分类。经常会遇到这样一些方法: 它们不是公共API的一部分,然而却非常适合在程序库之内使用。此时应该创建Private分类,如果程序库中的某个地方要用到这些方法,那就引入此分类的头文件。而分类的头文件并不随程序库一并公开,于是该库的使用者也就不知道库里面还有这些私有方法了。

总是为第三方类的分类名称加前缀

分类机制通常用于向无源码的既有类中新增功能。这个特性极为强大,但在使用时也很容易忽视其中可能产生的问题。这个问题在于:分类中的方法是直接添加在类里面的。它们就好比这个类中的固有方法。将分类方法加入类中这一操作是在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次覆盖,比如某个分类中的方法覆盖了 “主实现” 中的相关方法,而另外一个分类中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后一个分类为准。

比方说,要给NSString 添加分类,并在其中提供一些辅助方法,用于处理与 HTTP URL 有关的字符串。你可能会把分类写成这样:

@interface NSString (HTTP)
2 
3 // Encode a string with URL encoding
4 - (NSString *)urlEncodedString;
5 
6 // Decode a URL encoded string
7 - (NSString *)urlDecodedString;
8 
9 @end

现在看起来没什么问题,可是,如果还有一个分类也往 NSString 里面添加方法,那会如何呢?那个分类里可能也有个名叫 urlEncodedString 的方法,其代码与你所添加的大同小异,但却不能正确实现你所需的功能。那个分类的加载时机如果晚于你所写的这个分类,那么其代码就会把你的那一份覆盖掉,这样的话,你在代码中调用 urlEncodedString 方法时,实际执行的是那个分类里的实现代码。由于其执行结果和你预期的值不同,所以自己所写的那些代码也许就无法正常运行了。这种bug 很难追查,因为你可能意识不到实际执行的 urlEncodedString 代码并不是自己实现的那一份。

要解决此问题。一般的做法是: 以命名空间来区别各个分类的名称与其中所定义的方法。想在 Objective-C 中实现命名空间功能,只有一个办法,就是给相关名称都加上某个共用的前缀。与类名加前缀时所应考虑的因素相似,给分类所加的前缀也要选的恰当才行。一般来说,这个前缀应该与应用程序或程序库中其他地方所用的前缀相同。于是,我们可以给刚才那个 NSString 分类加上 ABC 前缀:

@interface NSString (ABC_HTTP)
2 
3 // Encode a string with URL encoding
4 - (NSString *)abc_urlEncodedString;
5 
6 // Decode a URL encoded string
7 - (NSString *)abc_urlDecodedString;
8 
9 @end

从技术角度讲,并不是非得用命名空间把各个分类的名称区隔开不可。即便两个分类重名了,也不会出错。然而这样做不好,编译器会发出类似下面这种警告信息:

warning: duplicate definition of category ‘HTTP’ on interface’NSString’

即便加了前缀,也难保其他分类不会覆盖你所写的方法,然而几率却小了很多。这样做也能避免类的开发者以后在更新该类时所添加的方法与你在分类中添加的方法重名。比方说,假如苹果决定在 NSString 类里添加 urlEncodedString 方法,而你在分类中所写的方法又没加前缀,那么可能就会覆盖苹果公司的方法,这样做不合适,因为 NSString 类的其他使用者想得到由苹果公司实现的代码所输出的结果,而非你所返回的那个结果。还有一种可能,就是苹果公司编写的实现代码带有一些附加效果,该方法若为你所写的代码所覆盖,则会令对象内的数据互不一致,从而造成难于查找的 bug。

此外还要记住,如果向某个类的分类中加入方法,那么在应用程序中,该类的每个实例均可调用这些方法。比方说,若是向 NSString、NSArray、NSNumber 这种系统类里加入方法,那么这些类的每个实例均可调用你所加的方法,即便这些实例不是由你的代码创建出来的,也依然会如此。如果你无意中把自己分类里的方法名起的和其他分类一样,或是与第三方库所添分类中的方法重名了,那么就可能出现奇怪的 bug,因为你以为此方法执行的是自己所写的那份代码,然而实际上却不是。与之相似,刻意覆写分类中的方法也不好,尤其是当你把代码发布为程序库供其他开发者使用,而他们又要依赖系统中现存的功能时,更不应该这么做。若是其他开发者又覆写了同一个方法,那么情况会更糟,因为无法确定最后到底会执行哪份实现代码。这又一次说明了为何要给分类中的方法名加上前缀。

勿在分类中声明属性

属性是封装数据的方式。尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。原因在于,除了 “class-continuation 分类” 之外,其他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。

比方说一个表示个人信息的类,用分类机制将其代码分段。那么你可能会设计一个专门处理交友事务的分类,其中所有方法都与操作某人的朋友列表有关。若是不知道刚才讲的那个问题,可能就会把代表朋友列表的那项属性也放到 Friendship 分类里面去了:

@interface Person : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
 
- (instancetype)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;

@end

@implementation Person

// Methods
 
@end



@interface Person (Friendship)

@property (nonatomic, strong) NSArray *friends;
- (BOOL)isFriendsWith:(EOCPerson *)person;

@end

@implementation Person (Friendship)
  
// Methods
 
@end

编译这段代码时,编译器会给出如下警告信息:

warning: property ‘friends’ requires method ‘friends’ to be defined - use @dynamic or provide a method a method implementation in this category [-Wobjc-property-implementation]
warning: property ‘friends’ requires method ‘setFriends:’ to be defined - use @dynamic or provide a method implementation in this category [-Wobjc-property-implementation]

这段警告信息有点令人费解,意思是说此分类无法合成与 friends 属性相关的实例变量,所以开发者需要在分类中为该属性实现存取方法。此时可以把存取方法声明为 @dynamic,也就是说,这些方法等到运行期再提供,编译器目前是看不见的。如果决定使用消息转发机制在运行期拦截方法调用,并提供其实现,那么或许可以采用这种做法。
关联对象能够解决在分类中不能合成实例变量的问题。比方说,我们可以在分类中用下面这段代码实现存取方法:

#import <objc/runtime.h>

static const char *kFriendsPropertyKey = "kFriendsPropertyKey";

@implementation Person (Friendship)

- (NSArray *)friends {
    return objc_getAssociatedObject(self, kFriendsPropertyKey);
}

- (void)setFriends:(NSArray *)friends {
    objc_setAssociatedObject(self, kFriendsPropertyKey, friends, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

这样做可行,但不太理想。要把相似的代码写很多遍,而且在内存管理问题上容易出错,因为我们在为属性实现存取方法时,经常会忘记遵从其内存管理语义。比方说,你可能通过属性特质修改了某个属性的内存管理语义。而此时还要记得,在设置方法中也得修改设置关联对象时所用的内存管理语义才行。所以说,尽管这个做法不坏,但是不推荐。
此外,你可能会选用可变数组来实现 friends 属性所对应的实例变量。若是这样做,就得在设置方法中将传入的数组参数拷贝为可变版本,而这又成为另外一个编码时容易出错的地方。因此,把属性定义在 “主接口”(main interface)中要比定义在分类里清晰得多。
在本例中,正确做法是把所有属性都定义在主接口里。类所封装的全部数据都应该定义在主接口中,这里是唯一能够定义实例变量(也就是数据)的地方。而属性只是定义实例变量及相关存取方法所用的 “语法糖” ,所以也应遵循同实例变量一样的规则。至于分类机制,则应将其理解为一种手段,目标在与扩展类的功能,而非封装数据。
虽说如此,但有时候只读属性还是可以在分类中使用的。比方说,要在 NSCalendar 类中创建分类,以返回包含各个月份名称的字符串数组。由于获取方法并不访问数据,而且属性也不需要由实例变量来实现,所以可像下面这样来实现此分类:

#import "NSCalendar+Additions.h"

@implementation NSCalendar (Additions)

- (NSArray *)allMonths {
    if ([self.calendarIdentifier isEqualToString:NSGregorianCalendar]) {
        return @[@"January", @"February", @"March", @"April", @"May", @"June", @"July", @"August", @"September", @"October", @"November", @"December"];
    } else if (/* other calendar identifiers */) {
        /* return months for other calendars */
    }
}

@end

由于实现属性所需的全部方法(在本例中,属性是只读的,所以只需实现一个方法)都已实现,所以不会再为该属性自动合成实例变量了。于是,编译器也就不会发出警告信息。然而,即便这种情况下,也最好不要用属性。属性所要表达的意思是: 类中有数据在支持着它。属性是用来封装数据的。在本例中,应该直接声明一个方法,用以获取月份名称列表:

@interface NSCalendar (Additions)

- (NSArray *)allMonths;
 
@end

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

“class-continuation 分类” 和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。与其他分类不同,“class-continuation 分类”没有名字,其写法如下:

@interface Person ()
// methods
@end

为什么需要有这种分类呢?因为其中可以定义方法和实例变量。为什么能在其中定义方法和实例变量呢? 只因有 “稳固的 ABI” 这一机制,使得我们无须知道对象大小即可使用它。由于类的使用者不一定需要知道实例变量的内存布局,所以,它们也就未必非得定义在公共接口中了。基于上述原因,我们可以像在类的实现文件里那样,于 “class-continuation 分类” 中给类新增实例变量。只需在适当位置上多写几个括号,然后把实例变量放进去:

@interface Person () {
    NSString *_anInstanceVariable;
}
// Method declarations here
@end

@implementation Person {
    int _anotherInstanceVariable;
}
// Method implementation here
@end

这样做有什么好处呢?公共接口里本来就能定义实例变量。不过,把它们定义在 “class-continuation 分类” 或 “实现块” 中可以将其隐藏起来,只供本类使用。 即便在公共接口里将其标注为 private,也还是会泄漏实现细节。比方说,你有个绝密的类,不想给其他人知道。假设你所写的某个类拥有那个绝密类的实例,而这个实例变量又声明在公共接口里面:

#import <Foundation/Foundation.h>

@class SuperSecretClass;

@interface Class : NSObject {
@private
    SuperSecretClass *_secretInstance;
}
@end

那么,信息就泄漏了,别人就会知道有个名叫 SuperSecretClass 的类。为解决此问题,可以不把实例变量声明为强类型,而是将其类型由 SuperSecretClass * 改为 id。然而这么做不够好,因为在类的内部使用此实例时,无法的得到编译器的帮助。没必要只因为想对外界隐藏某个内容就放弃编译器的辅助检查功能吧?这个问题可以由 “class-continuation 分类” 来解决。那个代表绝密类的实例可以声明成这样:

#import "Class.h"
#import "SuperSecretClass.h"

@interface Class () {
    SuperSecretClass *_secretInstance;
}

@end

@implementation Class
// Methods here
@end

实例变量也可以定义在实现块里,从语法上说,这与直接添加到 “class-continuation 分类” 等效,只是看个人喜好了。我更喜欢将其添加在 “class-continuation 分类” 中,以便将全部数据定义都放在一处。由于 “class-continuation 分类” 里还能定义一些属性,所以在这里额外声明一些实例变量也很合适。这些实例变量并非真的私有,因为在运行期总可以调用某些方法绕过此限制,不过,从一般意义来说。它们还是私有的。此外,由于没有声明在公共头文件里,所以将代码作为程序库的一部分来发行时,其隐藏程度更好。
编写 Objective-C++ 代码时 “class-continuation 分类” 也尤为有用。Objective-C++ 是 Objective-C 与 C++ 的混合体,其代码可以用这两种语言来编写。由于兼容性原因,游戏后端一般用 C++ 来写。另外,有时候要使用的第三方库可能只有 C++ 绑定,此时也必须使用 C++ 来编码。在这些情况下,使用 “class-continuation 分类” 会很方便。假设某个类打算这样写:

#import <Foundation/Foundation.h>
#include "SomeCppClass.h"

@interface Class : NSObject {
@private
    SomeCppClass _cppClass;
}
@end

该类的实现文件可能叫做 Class.mm,其中 .mm 扩展名表示编译器应该将此文件按 Objective-C++ 来编译,否则,就无法正确引入 SomeCppClass.h 了。然而请注意,名为 SomeCppClass 的这个 C++ 类必须完全引入,因为编译器要完整地解析其定义方能得知 _cppClass 实例变量的大小。于是,只要是包含 Class.h 的类,都必须编译为 Objective-C++ 才行,因为它们都引入了 SomeCppClass 类的头文件。这很快就会失控,最终导致整个应用程序全部都要编译为 Objective-C++。这么做确实完全可行,不过相当别扭,尤其是将代码发布为程序库供其他应用程序使用时,更不应该如此。要求第三方开发者将其源文件扩展名均改为 .mm 不是很合适。
你可能认为解决此问题的办法是:不引入 C++ 类的头文件,只是向前声明该类,并且将实例变量做成指向此类的指针。

#import <Foundation/Foundation.h>

@class SomeCppClass;

@interface Class : NSObject {
@private 
    SomeCppClass *_cppClass;
} 
@end

现在实例变量必须是指针,若不是,则编译器无法得知其大小,从而会报错。但所有指针的大小确实都是固定的,于是编译器只需要知道其所指的类型即可。不过,这么做还是会遇到刚才那个问题,因为引入 Class 头文件的源码里都包含 class 关键字,而这是 C++ 的关键字,所以仍然需要按 Objective-C++ 来编译才行。这样做既别扭又无必要,因为该实例变量毕竟是 private 的,其他类为什么要知道它呢?这个问题还是得用 “class-continuation 分类” 来解决。将刚才那个类改写之后,其代码如下:

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

@interface Class : NSObject

@end

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

@interface Class () {
    SomeCppClass _cppClass;
}

@end

@implementation Class

@end

改写后的 Class 类,其头文件里就没有 C++ 代码了,使用头文件的人甚至意识不到其底层实现代码中混有 C++ 成分。某些系统库用到了这种模式,比如网页浏览器框架 WebKit,其大部分代码都以 C++ 编写,然而对外展示出来的却是一套整洁的 Objective-C 接口。 CoreAnimation 里面也用到了此模式,它的许多后端代码都用 C++ 写成,但对外公布的却是一套纯 Objective-C 接口。
“class-continuation 分类” 还有一种合理用法,就是将 public 接口中声明为 “只读” 的属性扩展为 “可读写”,以便在类的内部设置其值。我们通常不直接访问实例变量,而是通过设置访问方法来做,因为这样能够触发 “键值观测” (Key-Value Observing,KVO)通知,其他对象有可能正监听此事件。出现在 “class-continuation 分类” 或其他分类中的属性必须同类接口里的属性具备相同的特质(attribute),不过,其 “只读”状态可以扩充为 “可读写”。例如,有个描述个人信息的类,其公共接口如下:

#import <Foundation/Foundation.h>

@interface Person : NSObject

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

- (instancetype)initWithFirstName:(NSString *)firstName
                      andLastName:(NSString *)lastName;

@end

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

@interface Person ()

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

只需要用上面几行代码就行了。现在 Person 的实现代码可以随意调用 “setFirstName:” 或 “setLastName:” 这两个设置方法,也可以用 “点语法” 来设置属性。这样做很有用,既能令外界无法修改对象,又能在其内部按照需要管理其数据。这样,封装在类中的数据就由实例本身来控制,而外部代码则无法修改其值。请注意,若观察者(observer)(也称 “监听器”) 正读取属性值而内部代码又在写入该属性时,则有可能引发 “竞争条件”(race condition)。合理使用同步机制能缓解此问题。
只会在类的实现代码中用到的私有方法也可以声明在 “class-continuation 分类” 中。这么做比较合适,因为它描述了那些只在类实现代码中才会使用的方法。这些方法可以这样写:

@interface Person ()
- (void)p_privateMethod;
@end

新版编译器不强制要求开发者在使用方法之前必须先声明。然而像上面这样在 “class-continuation 分类” 中声明一下通常还是有好处的,因为这样做可以把类里所含的相关方法都统一描述于此。在编写类的实现代码之前,像这样先把方法原型写出来,然后再逐个实现。要想使类的代码更易读懂,可以试试这个好办法。
最后还要讲一种用法:若对象所遵从的协议只应视为私有,则可在 “class-continuation 分类” 中声明。有时由于对象所遵从的某个协议在私有 API 中,所以我们可能不太想在公共接口中泄漏这一信息。比方说,Person 遵从了名为 SecretDelegate 的协议。如果声明在公共接口里,那么要像下面这样来写:

#import <Foundation/Foundation.h>
#import "SecretDelegate.h"

@interface Person : NSObject <SecretDelegate>

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

- (instancetype)initWithFirstName:(NSString *)firstName
                      andLastName:(NSString *)lastName;

@end

你可能会想,只需要向前声明 SecretDelegate 协议就可以不引入它了(或者说,不引入定义该协议的头文件了)。用下面这行向前声明语句来取代 #import 指令:

@protocol EOCSecretDelegate;

但是这样一来,只要引入 Person 头文件的地方,编译器都会给出下列警告信息:

warning:cannot find protocol definition for ‘SecretDelegate’

由于编译器看不到协议的定义,所以无法得知其中所含的方法,于是就会像这样警告开发者。然而这毕竟是个私有的内部协议,你甚至连名字都不想给别人知道。此时还得请 “class-continuation 分类” 来帮忙。不要在公共接口中声明 Person 类遵从了 SecretDelegate 协议,而是改到 “class-continuation 分类” 里面声明:

#import "Person.h"
#import "SecretDelegate.h"

@interface Person () <SecretDelegate>
@end

@implementation Person
/*...*/
@end

这个私有协议现在已经不为外界所知了,使用 Person 的人若不深入摸索一番,则很难发现其身影。

通过协议提供匿名对象

协议定义了一系列的方法,遵从此协议应该实现它们。于是,我们可以用协议把自己所写的API隐藏起来,将返回的对象设计为遵从此协议的纯id类型。这样的话,想要隐藏的类名就不会出现在API之中了。若是接口背后有多个不同的实现类,而你又不想指明具体实现哪个类,那么可以考虑用这个办法——因为有时候这些类可能会变,有时候它们又无法容纳与标准的类继承体系中,因而不能以某个公共基类来统一表示。
此概念经常称为“匿名对象”。例如在定义受委托者这个属性时,可以这么写:

@property (nonatomic, weak) id <Delegate> delegate;

由于该属性的类型是 id ,所以实际上任何类的对象都能充当这一属性。
有时对象类型并不重要,重要的是对象有没有实现某种方法,在此情况下,也可以用这些“匿名类型”来表达这一概念。即便实现代码总是使用固定的类,你可能还是会把他写成遵从某协议的匿名类型,以表示类型在此处不重要。

参考文献

文章来源于《Effective Objective - C 2.0》

猜你喜欢

转载自blog.csdn.net/streamery/article/details/105883606