類中經(jīng)常會(huì)包含一些無(wú)須對(duì)外公布的方法及實(shí)例變量。其實(shí)這些內(nèi)容也可以對(duì)外公布庵寞,并且寫(xiě)明其為私有枉证,開(kāi)發(fā)者不應(yīng)依賴它們以躯。Objective-C動(dòng)態(tài)消息系統(tǒng)(參見(jiàn)第11條)的工作方式?jīng)Q定了其不可能實(shí)現(xiàn)真正的私有方法或私有實(shí)例變量刮吧。然而湖饱,我們最好還是只把確實(shí)需要對(duì)外公布的那部分內(nèi)容公開(kāi)。那么杀捻,這種不需要對(duì)外公布但卻應(yīng)該具有的方法及實(shí)例變量應(yīng)該怎么寫(xiě)呢井厌?此時(shí),這個(gè)特殊的"class-continuation分類"就派上用場(chǎng)了致讥。
"class-continuation分類"和普通的分類不同仅仆,它必須定義在其所接續(xù)的那個(gè)類的實(shí)現(xiàn)文件里。其重要之處在于垢袱,這是唯一能聲明的實(shí)例變量的分類墓拜,而且此分類沒(méi)有特定的實(shí)現(xiàn)文件,其中的方法都應(yīng)該定義在類的主實(shí)現(xiàn)文件里请契。與其他分類不同咳榜,"class-continuation分類"沒(méi)有名字。比如爽锥,有個(gè)類叫做EOCPerson涌韩,其"class-continuation分類"寫(xiě)法如下:
@interface EOCPerson ()
//Methods here
@end
為什么需要有這種分類呢?因?yàn)槠渲锌梢远x方法和實(shí)現(xiàn)變量氯夷。為什么能在其中定義方法和實(shí)例變量呢臣樱?只因有"穩(wěn)固的ABI"這一機(jī)制(第6條詳解了此機(jī)制),使得我們無(wú)須知道對(duì)象大小即可使用它腮考。由于類的使用者不一定需要知道實(shí)例變量的內(nèi)存布局雇毫,所以,它們也就未必非得定義在公共接口中了踩蔚∽炻#基于上述原因,我們可以像在類的實(shí)現(xiàn)文件里那樣寂纪,于"class-continuation分類"中給類新增實(shí)例變量席吴。只需在適當(dāng)位置上多寫(xiě)幾個(gè)括號(hào),然后把實(shí)例變量放進(jìn)去:
@interface ECOPerson() {
NSString *_anInstanceVariable;
}
//Method declarations here
@end
@implementation EOCPerson {
int _anotherInstanceVariable;
}
//Method implementations here
@end
這樣做有什么好處呢?公共接口里本來(lái)就能定義實(shí)例變量捞蛋。不過(guò)孝冒,把它們定義在"class-continuation分類"或"實(shí)現(xiàn)塊"中可以將其隱藏起來(lái),只供本類使用拟杉。即便在公共接口里將其標(biāo)注為private庄涡,也還是會(huì)泄漏實(shí)現(xiàn)細(xì)節(jié)。比方說(shuō)搬设,你有個(gè)絕密的類穴店,不想給其他人知道撕捍。假設(shè)你所寫(xiě)的某個(gè)類擁有那個(gè)絕密類的實(shí)例,而這個(gè)實(shí)例變量又聲明在公共接口里面:
#import <Foundation/Foundation.h>
@class EOCSuperSecretClass
@interface EOCClass : NSObject {
@private
EOCSuperSecretClass *_secretInstance;
}
@end
那么泣洞,信息就泄漏了忧风,別人就會(huì)知道有個(gè)名叫EOCSuperSecretClass的類。為解決此問(wèn)題球凰,可以不把實(shí)例變量聲明為強(qiáng)類型狮腿,而是將其類型由EOCSuperSecretClass*改為id。然而這么做不夠好呕诉,因?yàn)樵陬惖膬?nèi)部使用此實(shí)例時(shí)缘厢,無(wú)法得到編譯器的幫助。沒(méi)必要只因?yàn)橄雽?duì)外界隱藏某個(gè)內(nèi)容就放棄編譯器的輔助檢查功能吧甩挫?這個(gè)問(wèn)題可以由"class-continuation分類"來(lái)解決贴硫。那個(gè)代表絕密類的實(shí)例可以聲明成這樣:
// 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
實(shí)例變量也可以定義在實(shí)現(xiàn)塊里,從語(yǔ)法上說(shuō)伊者,這與直接添加到"class-continuation分類"等效英遭,只是看個(gè)人喜好了。筆者喜歡將其添加在"class-continuation分類"中删壮,以便將全部數(shù)據(jù)定義都放在一處贪绘。由于"class-continuation分類"里還能定義一些屬性兑牡,所以在這里額外聲明一些實(shí)例變量也很合適央碟。這些實(shí)例變量并非真的私有,因?yàn)樵谶\(yùn)行期總可以調(diào)用某些方法繞過(guò)此限制均函,不過(guò)亿虽,從一般意義上來(lái)說(shuō),它們還是私有的苞也。此外洛勉,由于沒(méi)有聲明在公共頭文件里,所以將代碼作為程序庫(kù)的一部分來(lái)發(fā)行時(shí)如迟,其隱藏程度更好收毫。
編寫(xiě)Objective-C代碼時(shí)"class-continuation分類"也尤為有用。Objective-C++是Objective-C與C++的混合體殷勘,其代碼可以用這兩種語(yǔ)言來(lái)編寫(xiě)此再。由于兼容性原因,游戲后端一般用C++來(lái)寫(xiě)玲销。另外输拇,有時(shí)候要使用的第三方庫(kù)可能只有C++綁定,此時(shí)也必須使用C++來(lái)編碼贤斜。在這些情況下策吠,使用"class-continuation分類"會(huì)很方便逛裤。假設(shè)某個(gè)類打算這樣寫(xiě):
#import <Foundation/Foundation.h>
#include "SomeCppClass.h"
@interface EOCClass : NSObject {
@private
SomeCppClass _cppClass;
}
@end
該類的實(shí)現(xiàn)文件可能叫做EOCClass.mm,其中.mm擴(kuò)展名表示編譯器應(yīng)該將此文件按Objective-C++來(lái)編譯猴抹,否則带族,就無(wú)法正確引入SomeCppClass.h了。然而請(qǐng)注意洽糟,名為SomeCppClass的這個(gè)C++類必須完全引入炉菲,因?yàn)榫幾g器要完整地解析其定義方能得知_cppClass實(shí)例變量的大小。于是坤溃,只要是包含EOCClass.h的類拍霜,都必須編譯為Objective-C++才行,因?yàn)樗鼈兌家肓薙omeCppClass類的頭文件薪介。這很快就會(huì)失控祠饺,最終導(dǎo)致整個(gè)應(yīng)用程序全部都要編譯為Objective-C++。這么做確實(shí)完全可行汁政,不過(guò)筆者覺(jué)得相當(dāng)別扭道偷,尤其是將代碼發(fā)布為程序庫(kù)供其他應(yīng)用程序使用時(shí),更不應(yīng)該如此记劈。要求第三方開(kāi)發(fā)者將其源文件擴(kuò)展名均改為.mm不是很合適勺鸦。
你可能認(rèn)為解決此問(wèn)題的辦法是:不引入C++類的頭文件,只是向前聲明該類目木,并且將實(shí)例變量做成指向此類的指針换途。
#import <Foundation/Foundation.h>
class SomeCppClass;
@interface EOCClass : NSObject {
@private
SomeCppClass *_cppClass;
}
@end
現(xiàn)在實(shí)例變量必須是指針,若不是刽射,則編譯器無(wú)法得知其大小军拟,從而會(huì)報(bào)錯(cuò)。但所有指針的大小確實(shí)都是固定的誓禁,于是編譯器只需知道其所指的類型即可懈息。不過(guò),這么做還是會(huì)遇到剛才那個(gè)問(wèn)題摹恰,因?yàn)橐隕OCClass頭文件的源碼里都包含class關(guān)鍵字辫继,而這是C++的關(guān)鍵字,所以仍然需要按Objective-C++來(lái)編譯才行俗慈。這樣做既別扭又無(wú)必要姑宽,因?yàn)樵搶?shí)例變量畢竟是private的,其他類為什么要知道它呢姜盈?這個(gè)問(wèn)題還是得用"class-continuation分類"來(lái)解決低千。將剛才那個(gè)類改寫(xiě)之后,其代碼如下:
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject
@end
// EOCClass.mm
#import "EOCClass.h"
#include "SomeCppClass.h"
@interface EOCClass () {
SomeCppClass _cppClass;
}
@end
@implementation EOCClass
@end
改寫(xiě)后的EOCClass類,其頭文件里就沒(méi)有C++代碼了示血,使用頭文件的人甚至意識(shí)不到其底層實(shí)現(xiàn)代碼中混有C++成分棋傍。某些系統(tǒng)庫(kù)用到了這種模式,比如網(wǎng)頁(yè)瀏覽器框架WebKit难审,其大部分代碼都以C++編寫(xiě)瘫拣,然而對(duì)外展示出來(lái)的卻是一套整潔的Objective-C接口。CoreAnimation里面也用到了此模式告喊,它的許多后端代碼都用C++寫(xiě)成麸拄,但對(duì)外公布的卻是一套純Objective-C接口。
"class-continuation分類"還有一種合理用法黔姜,就是將public接口中聲明為"只讀"的屬性擴(kuò)展為"可讀寫(xiě)"拢切,以便在類的內(nèi)部設(shè)置其值。我們通常不直接訪問(wèn)實(shí)例變量秆吵,而是通過(guò)設(shè)置訪問(wèn)方法來(lái)做(參見(jiàn)第7條)淮椰,因?yàn)檫@樣能夠觸發(fā)"鍵值觀測(cè)"(Key-Value Observing, KVO)通知纳寂,其他對(duì)象有可能正監(jiān)聽(tīng)此事件主穗。出現(xiàn)在"class-continuation分類"或其他類中的屬性必須同類接口里的屬性具備相同的特質(zhì)(attribute),不過(guò)毙芜,其"只讀"狀態(tài)可以擴(kuò)充為"可讀寫(xiě)"忽媒。例如,有個(gè)描述個(gè)人信息的類腋粥,其公共接口如下:
#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
我們一般會(huì)在"class-continuation分類"中把這兩個(gè)屬性擴(kuò)展為"可讀寫(xiě)":
@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
只需要用上面幾行代碼就行了』抻辏現(xiàn)在EOCPerson的實(shí)現(xiàn)代碼可以隨意調(diào)用"setFirstName:"或"setLastName:"這兩個(gè)設(shè)置方法,也可以用"點(diǎn)語(yǔ)法"來(lái)設(shè)置屬性灯抛。這樣做很有用金赦,既能令外界無(wú)法修改對(duì)象音瓷,又能在其內(nèi)部按照需要管理其數(shù)據(jù)对嚼。這樣,封裝在類中的數(shù)據(jù)就由實(shí)例本身來(lái)控制绳慎,而外部代碼則無(wú)法修改其值纵竖。第18條曾詳述了這一話題。請(qǐng)注意杏愤,若觀察者(observer)正讀取屬性值而內(nèi)部代碼又在寫(xiě)入該屬性時(shí)靡砌,則有可能引發(fā)"競(jìng)爭(zhēng)條件"(race condition)。合理使用同步機(jī)制(參見(jiàn)第41條)能緩解此問(wèn)題珊楼。
只會(huì)在類的實(shí)現(xiàn)代碼中用到的私有方法也可以聲明在"class-continuation分類"中通殃。這么做比較合適,因?yàn)樗枋隽四切┲辉陬悓?shí)現(xiàn)代碼中才會(huì)使用的方法。這些方法可以這樣寫(xiě):
@interface EOCPerson()
- (void)p_privateMethod;
@end
此處根據(jù)第20條所述的建議為方法名加了前綴画舌,以體現(xiàn)其為私有方法堕担。新版編譯器不強(qiáng)制要求開(kāi)發(fā)者在使用方法之前必須先聲明。然而像上面這樣在"class-continuation分類"中聲明一下通常還是有好處的曲聂,因?yàn)檫@樣做可以把類里所含的相關(guān)方法都統(tǒng)一描述于此霹购。筆者在編寫(xiě)類的實(shí)現(xiàn)代碼之前,經(jīng)常喜歡像這樣先把方法原型寫(xiě)出來(lái)朋腋,然后在逐個(gè)實(shí)現(xiàn)齐疙。要想使類的代碼更易讀懂,可以試試這個(gè)好辦法旭咽。
最后還要講一種用法: 若對(duì)象所遵從的協(xié)議只應(yīng)視為私有贞奋,則可在"class-continuation分類"中聲明。有時(shí)由于對(duì)象所遵從的某個(gè)協(xié)議在私有API中穷绵,所以我們可能不太想在公共接口中泄漏這一信息忆矛。比方說(shuō),EOCPerson遵從了名為EOCSecretDelegate的協(xié)議请垛。如果聲明在公共接口里催训,那么要像下面這樣寫(xiě):
#import <Foundation/Foundation.h>
#import "EOCSecretDelegate.h"
@interface EOCPerson : NSObject <EOCSecretDelegate>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString*)firstName
lastName:(NSString*)lastName;
@end
你可能會(huì)說(shuō),只需要向前聲明EOCSecretDelegate協(xié)議就可以不引入它了(或者說(shuō)宗收,不引入定義該協(xié)議的頭文件了)漫拭。用下面這行向前聲明語(yǔ)句來(lái)取代#import指令:
@protocol EOCSecretDelegate;
但是這樣一來(lái),只要引入EOCPerson頭文件的地方混稽,編譯器都會(huì)給出下列警告信息:
warning: cannot find protocol definition for 'EOCSecretDelegate'
由于編譯器看不到協(xié)議的定義采驻,所以無(wú)法得知其中所含的方法,于是就會(huì)像這樣警告開(kāi)發(fā)者匈勋。然而礼旅,這畢竟是個(gè)私有的內(nèi)部協(xié)議,你甚至連名字都不想給別人知道洽洁。此時(shí)還得請(qǐng)"class-continuation分類"來(lái)幫忙痘系。不要在公共接口中聲明EOCPerson類遵從了EOCSecretDelegate協(xié)議,而是改到"class-continuation分類"里面聲明:
#import "EOCPerson.h"
#import "EOCSecretDelegate.h"
@interface EOCPerson () <EOCSecretDelegate>
@end
@implementation EOCPerson
…
@end
公共接口內(nèi)所有提到EOCSecretDelegate的地方都可刪去饿自。這個(gè)私有協(xié)議現(xiàn)在已經(jīng)不為外界所知了汰翠,使用EOCPerson的人若不深入探索一番,則很難發(fā)現(xiàn)其身影昭雌。
要點(diǎn)
- 通過(guò)"class-continuation分類"向類中新增實(shí)例變量
-如果某屬性在主接口中聲明為"只讀"复唤,而類的內(nèi)部又要用設(shè)置方法修改此屬性,那么就在"class-continuation分類"中將其擴(kuò)展為"可讀寫(xiě)"烛卧。 - 把私有方法的原型聲明在"class-continuation分類"里面佛纫。
- 若想使類所遵循的協(xié)議不為人所知,則可于"class-continuation分類"中聲明。