iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法 - 篇1/4
iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法 - 篇2/4
iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法 - 篇3/4
iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法 - 篇4/4
- 第二章 對(duì)象、消息、運(yùn)行期
- 第11條:理解objc_msgSend的作用
- 第12條:理解消息轉(zhuǎn)發(fā)機(jī)制
- 第13條:用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”
- 第14條:理解“類對(duì)象”的用意
- 第三章 接口與API設(shè)計(jì)
- 第15條:用前綴避免命名空間沖突
- 第16條:提供“全能初始化方法”
- 第17條:實(shí)現(xiàn)description方法
- 第18條:盡量使用不可變對(duì)象
- 第19條:使用清晰而協(xié)調(diào)的命名方式
- 第20條:為私有方法名加前綴
- 第21條:理解Objective-C錯(cuò)誤模型
- 第22條:理解NSCopying協(xié)議
- 第四章 協(xié)議與分類
- 第23條:通過(guò)委托與數(shù)據(jù)源協(xié)議進(jìn)行對(duì)象間通信
- 第24條:將類的實(shí)現(xiàn)代碼分散到便于管理的數(shù)個(gè)分類當(dāng)中
- 第25條:總是為第三方類的分類名稱加前綴
- 第26條:勿在分類中聲明屬性
- 第27條:使用"class - continuation"分類隱藏實(shí)現(xiàn)細(xì)節(jié)
- 第28條:通過(guò)協(xié)議提供匿名對(duì)象
第11條:理解objc_msgSend的作用
在對(duì)象上調(diào)用方法是Objective-C中經(jīng)常使用的功能。用Objective-C的術(shù)語(yǔ)來(lái)說(shuō),這叫做“傳遞消息”(pass a message)。
由于Objective-C是C的超集,所以最好先理解C語(yǔ)言的函數(shù)調(diào)用方式。C語(yǔ)言使用“靜態(tài)綁定”(static binding),也就是說(shuō),在編譯期就能決定運(yùn)行時(shí)所應(yīng)調(diào)用的函數(shù)。
import <stdio.h>
void printHello {
printf ("Hello, world! \n");
}
void printGoodbye() {
printf ("Goodbye, world! \n");
}
void doTheThing(int type) {
if (type == 0) {
printHello();
} else {
printGoodbye();
}
return 0;
}
如果不考慮“內(nèi)聯(lián)”(inline),那么編譯器在編譯代碼的時(shí)候就已經(jīng)知道程序中有該函數(shù)了,于是會(huì)直接生成調(diào)用這些函數(shù)的指令。而函數(shù)地址實(shí)際上是硬編碼在指令之中的。若是將剛才那段代碼寫成下面這樣,會(huì)如何?
void doTheThing(int type) {
void (*fnc)();
if (type == 0) {
fnc = printHello;
} else {
fnc = printGoodbye;
}
fnc ();
return 0;
}
這時(shí)就得使用“動(dòng)態(tài)綁定”(dynamic binding) 了,因?yàn)樗{(diào)用的函數(shù)直到運(yùn)行期才能確定。編譯器在這種情況下生成的指令與剛才那個(gè)例子不同,在第一個(gè)例子中,if與else語(yǔ)句里都有函數(shù)調(diào)用指令。而在第二個(gè)例子中,只有一個(gè)函數(shù)調(diào)用指令,不過(guò)待調(diào)用的函數(shù)地址無(wú)法硬編碼在指令之中,而是要在運(yùn)行期讀取出來(lái)。
在Objective-C中,如果向某對(duì)象傳遞消息,那就會(huì)使用動(dòng)態(tài)綁定機(jī)制來(lái)決定需要調(diào)用的方法。在底層,所有方法都是普通的C語(yǔ)言函數(shù),然而對(duì)象收到消息之后,究竟該調(diào)用哪個(gè)方法則完全于運(yùn)行期決定,甚至可以在程序運(yùn)行時(shí)改變,這些特性使得Objective-C成為一門真正的動(dòng)態(tài)語(yǔ)言。
給對(duì)象發(fā)送消息可以這樣來(lái)寫:
id returnValue = [someObject messageName:parameter];
在本例中,someObject 叫做“接收者”(receiver), messageName 叫做“選擇子”(selector),選擇子指的就是方法的名字, “選擇子”與“方法”這兩個(gè)詞經(jīng)常交替使用。選擇子與參數(shù)合起來(lái)稱為“消息”(message)。編譯器看到此消息后,將其轉(zhuǎn)換為一條標(biāo)準(zhǔn)的C語(yǔ)言函數(shù)調(diào)用,所調(diào)用的函數(shù)乃是消息傳遞機(jī)制中的核心函數(shù),叫做objc_msgSend,其 “原型"(prototype)如下:
void objc_msgSend(id self, SEL cmd,...)
編譯器會(huì)把剛才那個(gè)例子中的消息轉(zhuǎn)換為如下函數(shù):
id returnValue = objc_msgSend(someObject,@selector(messageName:), parameter);
每個(gè)類里都有一張表格,其中的指針都會(huì)指向這種函數(shù),而選擇子的名稱則是査表時(shí)所用的“鍵”。objC_mSgSend等函數(shù)正是通過(guò)這張表格來(lái)尋找應(yīng)該執(zhí)行的方法并跳至其實(shí)現(xiàn)的。objc_msgSend函數(shù)會(huì)依據(jù)接收者與選擇子的類型來(lái)調(diào)用適當(dāng)?shù)姆椒ā榱送瓿纱瞬僮鳎?該方法需要在接收者所屬的類中搜尋其“方法列表”(list of methods),如果能找到與選擇子名稱相符的方法,就跳至其實(shí)現(xiàn)代碼。若是找不到,那就沿著繼承體系繼續(xù)向上査找,等找到合適的方法之后再跳轉(zhuǎn)。如果最終還是找不到相符的方法,那就執(zhí)行“消息轉(zhuǎn)發(fā)” (message forwarding)操作。
這么說(shuō)來(lái),想調(diào)用一個(gè)方法似乎需要很多步驟。所幸objc_mSgSend會(huì)將匹配結(jié)果緩存在“快速映射表"(fast map)里面,每個(gè)類都有這樣一塊緩存,若是稍后還向該類發(fā)送與選擇子相同的消息,那么執(zhí)行起來(lái)就很快了。當(dāng)然這種“快速執(zhí)行路徑”(fastpath)還是不如 “靜態(tài)綁定的函數(shù)調(diào)用操作’(statically bound function call)那樣迅速,不過(guò)只要把選擇子緩存起來(lái),也就不會(huì)慢很多,實(shí)際上,消息派發(fā)(message dispatch)并非應(yīng)用程序的瓶頸所在。假如真是個(gè)瓶頸的話,那你可以只編寫純C函數(shù),在調(diào)用時(shí)根據(jù)需要,把ObjectWe-C對(duì)象的狀態(tài)傳進(jìn)去。
前面講的這部分內(nèi)容只描述了部分消息的調(diào)用過(guò)程,其他“邊界情況"(edgecase)。則需要交由Objective-C運(yùn)行環(huán)境中的另一些函數(shù)來(lái)處理:
-
objc_msgSend_stret
:如果待發(fā)送的消息要返回結(jié)構(gòu)體,那么可交由此函數(shù)處理。只有當(dāng)CPU的寄存器能夠容納得下消息返回類型時(shí),這個(gè)函數(shù)才能處理此消息。若是返回值無(wú)法容納于CPU寄存器中(比如說(shuō)返回的結(jié)構(gòu)體太大了),那么就由另一個(gè)函數(shù)執(zhí)行派發(fā)。此時(shí),那個(gè)函數(shù)會(huì)通過(guò)分配在棧上的某個(gè)變量來(lái)處理消息所返回的結(jié)構(gòu)體。 -
objc_mSgSend_fpret
:如果消息返回的是浮點(diǎn)數(shù),那么可交由此函數(shù)處理。在某些架構(gòu)的CPU中調(diào)用函數(shù)時(shí),需要對(duì)“浮點(diǎn)數(shù)寄存器’(floating-point register)做特殊處理, 也就是說(shuō),通常所用的objC_msgSend在這種情況下并不合適。這個(gè)函數(shù)是為了處理 x86等架構(gòu)CPU中某些令人稍覺(jué)驚訝的奇怪狀況。 -
objc_msgSendSuper
:如果要給超類發(fā)消息,例如[super message:parameter],那么就交由此函數(shù)處理。也有另外兩個(gè)與objc_msgSend_stret和objc_msgSend_fpret等效的函數(shù),用于處理發(fā)給super的相應(yīng)消息。
利用“尾調(diào)用優(yōu)化”技術(shù),令“跳至方法實(shí)現(xiàn)”這一操作變得更簡(jiǎn)單些。
如果某函數(shù)的最后一項(xiàng)操作是調(diào)用另外一個(gè)函數(shù),那么就可以運(yùn)用“尾調(diào)用優(yōu)化”技術(shù)。 編譯器會(huì)生成調(diào)轉(zhuǎn)至另一函數(shù)所需的指令碼,而且不會(huì)向調(diào)用堆棧中推入新的“棧幀"(frame stack)。只有當(dāng)某函數(shù)的最后一個(gè)操作僅僅是調(diào)用其他函數(shù)而不會(huì)將其返回值另作他用時(shí), 才能執(zhí)行“尾調(diào)用優(yōu)化”。這項(xiàng)優(yōu)化對(duì)objc_mSgSend非常關(guān)鍵,如果不這么做的話,那么每次調(diào)用Objective-C方法之前,都需要為調(diào)用objC_mSgSend函數(shù)準(zhǔn)備“棧幀”,大家在“棧蹤跡”(stack trace)中可以看到這種“棧幀”。此外,若是不優(yōu)化,還會(huì)過(guò)早地發(fā)生“棧溢出” (stack overflow)現(xiàn)象。
這樣也就理解,為何在調(diào)試的時(shí)候,棧“回溯”(backtrace)信息中總是出現(xiàn)objC_mSgSend。
要點(diǎn):
- 消息由接收者、選擇子及參數(shù)構(gòu)成。給某對(duì)象潑送消息"(invoke a message:也是“調(diào)用”的意思,此處為了與“call”相區(qū)隔,將其臨時(shí)譯為“發(fā)送”,也可理解為“激發(fā)”、 “觸發(fā))”也就相當(dāng)于在該對(duì)象上“調(diào)用方法”(call a method)。
- 發(fā)給某對(duì)象的全部消息都要由“動(dòng)態(tài)消息派發(fā)系統(tǒng)”(dynamic message dispatch system) 來(lái)處理,該系統(tǒng)會(huì)査出對(duì)應(yīng)的方法,并執(zhí)行其代碼。
第12條:理解消息轉(zhuǎn)發(fā)機(jī)制
上面講解了對(duì)象的消息傳遞機(jī)制,本節(jié)講解對(duì)象在收到無(wú)法解讀的消息之后會(huì)發(fā)生什么情況。
若想令類能理解某條消息,我們必須以程序碼實(shí)現(xiàn)出對(duì)應(yīng)的方法才行。但是,在編譯期向類發(fā)送了其無(wú)法解讀的消息并不會(huì)報(bào)錯(cuò),因?yàn)樵谶\(yùn)行期可以繼續(xù)向類中添加方法,所以編譯器在編譯時(shí)還無(wú)法確知類中到底會(huì)不會(huì)有某個(gè)方法實(shí)現(xiàn)。當(dāng)對(duì)象接收到無(wú)法解讀的消息后,就會(huì)啟動(dòng)“消息轉(zhuǎn)發(fā)"(message forwarding)機(jī)制,程序員可經(jīng)由此過(guò)程告沂對(duì)象應(yīng)該如何處理未知消息。
你可能早就遇到過(guò)經(jīng)由消息轉(zhuǎn)發(fā)流程所處理的消息了,只是未加留意。如果在控制臺(tái)中看到下面這種提示信息,那就說(shuō)明你曾向某個(gè)對(duì)象發(fā)送過(guò)一條其無(wú)法解讀的消息,從而啟動(dòng)了消息轉(zhuǎn)發(fā)機(jī)制,并將此消息轉(zhuǎn)發(fā)給了NSObject的默認(rèn)實(shí)現(xiàn)。
- [_NSCFNumber lowercasestring]: unrecognized selector sent to instance 0x87
*** Terminating app due to uncaught exception
'NSInvalidArgumentException',reason: '-[_ NSCFNumber lowercasestring]: unrecognized selector sent to instance 0x87?
上面這段異常信息是由NSObject的doesNotRecognizeSelector:
方法所拋出的,此異常表明:消息接收者的類型是__NSCFNumber,而該接收者無(wú)法理解名為lowercaseString
的選擇子。本例所列舉的這種情況并不奇怪,因?yàn)镹SNumber類里本來(lái)就沒(méi)有名為lowercaseString的方法。控制臺(tái)中看到的那__NSFCNumber是為了實(shí)現(xiàn)“無(wú)縫橋接"(toll-free bridging,后續(xù)將會(huì)詳解此技術(shù))而使用的內(nèi)部類(internal class),配置NSNumber對(duì)象時(shí)也會(huì)一并創(chuàng)建此對(duì)象。在本例中,消息轉(zhuǎn)發(fā)過(guò)程以應(yīng)用程序崩潰而告終,不過(guò),開發(fā)者在編寫自己的類時(shí),可于轉(zhuǎn)發(fā)過(guò)程中設(shè)置掛鉤,用以執(zhí)行預(yù)定的邏輯,而不使應(yīng)用程序崩潰。
消息轉(zhuǎn)發(fā)分為兩大階段。第一階段先征詢接收者,所屬的類,看其是否能動(dòng)態(tài)添加方法,以處理當(dāng)前這個(gè)“未知的選擇子"(unknown selector),這叫做“動(dòng)態(tài)方法解析”(dynamic method resolution)。第二階段涉及“完整的消息轉(zhuǎn)發(fā)機(jī)制”(ftill forwarding mechanism)。如果運(yùn)行期系統(tǒng)已經(jīng)把第一階段執(zhí)行完了,那么接收者自己就無(wú)法再以動(dòng)態(tài)新增方法的手段來(lái)響應(yīng)包含該選擇子的消息了。此時(shí),運(yùn)行期系統(tǒng)會(huì)請(qǐng)求接收者以其他手段來(lái)處理與消息相關(guān)的方法調(diào)用。這又細(xì)分為兩小步。首先,請(qǐng)接收者看看有沒(méi)有其他對(duì)象能處理這條消息。若有,則運(yùn)行期系統(tǒng)會(huì)把消息轉(zhuǎn)給那個(gè)對(duì)象,于是消息轉(zhuǎn)發(fā)過(guò)程結(jié)束,一切如常。若沒(méi)有“備援的接收者”(replacement receiver),則啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制,運(yùn)行期系統(tǒng)會(huì)把與消息有關(guān)的全部細(xì)節(jié)都封裝到NSInvocation對(duì)象中,再給接收者最后一次機(jī)會(huì),令其設(shè)法解決當(dāng)前還未處理的這條消息。
動(dòng)態(tài)方法解析
對(duì)象在收到無(wú)法解讀的消息后,首先將調(diào)用其所屬類的下列類方法:
+ (BOOL)resolvelnstanceMethod:(SEL)selector
該方法的參數(shù)就是那個(gè)未知的選擇子,其返回值為Boolean類型,表示這個(gè)類是否能新增一個(gè)實(shí)例方法用以處理此選擇子。在繼續(xù)往下執(zhí)行轉(zhuǎn)發(fā)機(jī)制之前,本類有機(jī)會(huì)新增一個(gè)處理此選擇子的方法。假如尚未實(shí)現(xiàn)的方法不是實(shí)例方法而是類方法,那么運(yùn)行期系統(tǒng)就會(huì)調(diào)用另外一個(gè)方法,該方法與resolvelnstanceMethod:
類似,叫做resolveClassMethod:
。
使用這種辦法的前提是:相關(guān)方法的實(shí)現(xiàn)代碼已經(jīng)寫好,只等著運(yùn)行的時(shí)候動(dòng)態(tài)插在類里面就可以了。此方案常用來(lái)實(shí)現(xiàn)@dynamic屬性,因?yàn)閷?shí)現(xiàn)這些屬性所需的存取方法在編譯期就能確定。
備援接收者
當(dāng)前接收者還有第二次機(jī)會(huì)能處理未知的選擇子,在這一步中,運(yùn)行期系統(tǒng)會(huì)問(wèn)它:能不能把這條消息轉(zhuǎn)給其他接收者來(lái)處理。與該步驟對(duì)應(yīng)的處理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector
方法參數(shù)代表未知的選擇子,若當(dāng)前接收者能找到備援對(duì)象,則將其返回,若找不到, 就返回nil。通過(guò)此方案,我們可以用“組合”(composition)來(lái)模擬出“多重繼承”(multiple inheritance)的某些特性。在一個(gè)對(duì)象內(nèi)部,可能還有一系列其他對(duì)象,該對(duì)象可經(jīng)由此方法將能夠處理某選擇子的相關(guān)內(nèi)部對(duì)象返回,這樣的話,在外界看來(lái),好像是該對(duì)象親自處理了這些消息似的。
請(qǐng)注意,我們無(wú)法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息。若是想在發(fā)送給備援接收者之前先修改消息內(nèi)容,那就得通過(guò)完整的消息轉(zhuǎn)發(fā)機(jī)制來(lái)做了。
完整的消息轉(zhuǎn)發(fā)
如果轉(zhuǎn)發(fā)算法已經(jīng)來(lái)到這一步的話,那么唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制 了。首先創(chuàng)建NSInvocation對(duì)象,把與尚未處理的那條消息有關(guān)的全部細(xì)節(jié)都封于其中。 此對(duì)象包含選擇子、目標(biāo)(target)及參數(shù)。在觸發(fā)NSInvocation對(duì)象時(shí),“消息派發(fā)系統(tǒng)” (message-dispatch system)將親自出馬,把消息指派給目標(biāo)對(duì)象。
此步驟會(huì)調(diào)用下列方法來(lái)轉(zhuǎn)發(fā)消息:
- (void)forwardlnvocation:(NSInvocation *)invocation
這個(gè)方法可以實(shí)現(xiàn)得很簡(jiǎn)單:只需改變調(diào)用目標(biāo),使消息在新目標(biāo)上得以調(diào)用即可。然而這樣實(shí)現(xiàn)出來(lái)的方法與“備援接收者”方案所實(shí)現(xiàn)的方法等效,所以很少有人采用這么簡(jiǎn)單的實(shí)現(xiàn)方式。比較有用的實(shí)現(xiàn)方式為:在觸發(fā)消息前,先以某種方式改變消息內(nèi)容,比如追加另外一個(gè)參數(shù),或是改換選擇子,等等。實(shí)現(xiàn)此方法時(shí),若發(fā)現(xiàn)某調(diào)用操作不應(yīng)由本類處理,則需調(diào)用超類的同名方法。這樣的話,繼承體系中的每個(gè)類都有機(jī)會(huì)處理此調(diào)用請(qǐng)求,直至NSObject。如果最后調(diào)用了 NSObject類的方法,那么該方法還會(huì)繼而調(diào)用doesNotRecognizeSelector:
以拋出異常, 此異常表明選擇子最終未能得到處理。
消息轉(zhuǎn)發(fā)全流程
接收者在每一步中均有機(jī)會(huì)處理消息。步驟越往后,處理消息的代價(jià)就越大。最好能在第一步就處理完,這樣的話,運(yùn)行期系統(tǒng)就可以將此方法緩存起來(lái)了。如果這個(gè)類的實(shí)例稍后還收到同名選擇子,那么根本無(wú)須啟動(dòng)消息轉(zhuǎn)發(fā)流程。
以完整的例子演示動(dòng)態(tài)方法解析
為了說(shuō)明消息轉(zhuǎn)發(fā)機(jī)制的意義,下面示范如何以動(dòng)態(tài)方法解析來(lái)實(shí)現(xiàn)@dynamic屬性。將屬性聲明為@dynamic,這樣的話,編譯器就不會(huì)為其自動(dòng)生成實(shí)例變量及存取方法了
#import <Foundation/Foundation.h>
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end
#import "EOCAutoDictionary.h"
#import <objc/runtime.h>
@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingstore;
@end
@implementation EOCAutoDictionary
@dynamic date,opaqueObject;
- (id)init {
if ([self == [super init]) {
_backingStore = [NSMutableDictionary new];
}
return self;
}
+ (BOOL)resolvelnstanceMethod:(SEL)selector {
NSString *selectorstring = NSStringFromSelector(selector);
if ([selectorstring hasPrefix: @"set"]) {
class_addMethod(self,selector,(IMP)autoDictionarySetter,"v@:@");
} else {
class_addMethod(self,selector,(IMP)autoDictionarySetter,"@@:");
}
return YES;
}
//getter函數(shù)可以用下列代碼實(shí)現(xiàn):
id autoDictionaryGetter(id self, SEL _cmd) {
//Get the backing store from the object
EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
//Thekey is simply the selector name
NSString *key = NSStringFromSelector(_cmd);
// Return the value
return [backingStore objectForKey:key];
}
//setter函數(shù)則可以這么寫:
void autoDictionarySetter(id self, SEL _cmd, id value) {
//Getthe backing store from the object
EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
//The selector will be for example, "setOpaqueObject: ". We need to remove the "set",and lowercase the first letter of the remainder.
NSString *selectorstring = NSStringFromSelector(_cmd);
NSMutablestring *key = [selectorstring mutableCopy];
// Remove the ':' at the end
[key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
// Remove the 'set' prefix
[key deleteCharactersInRange:NSMakeRange(0,3)];
// Lowercase the first character
NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercasestring];
[key replaceCharactersInRange:NSMakeRange(0,1) withString:lowercaseFirstChar];
if (value) {
[backingStore setObject:value forKey:key];
} else {
[backingStore removeObjectForKey:key];
}
}
@end
當(dāng)開發(fā)者首次在EOCAutoDictionary實(shí)例上訪問(wèn)某個(gè)屬性時(shí),運(yùn)行期系統(tǒng)還找不到對(duì)應(yīng)的選擇子,因?yàn)樗璧倪x擇子既沒(méi)有直接實(shí)現(xiàn),也沒(méi)有合成出來(lái)。現(xiàn)在假設(shè)要寫入opaqueObject
屬性,那么系統(tǒng)就會(huì)以setOpaqueObject:
為選擇子來(lái)調(diào)用上面這個(gè)方法。 同理,在讀取該屬性時(shí),系統(tǒng)也會(huì)調(diào)用上述方法,只不過(guò)傳入的選擇子是opaqueObject
。
resolvelnslanceMethod
方法會(huì)判斷選擇子的前綴是否為set,以此分辨其是set選擇子還是get選擇子。在這兩種情況下,都要向類中新增一個(gè)處理該選擇子所用的方法,這兩個(gè)方法分別以autoDictionarySetter
及autoDictionaryGetter
函數(shù)指針的形式出現(xiàn)。此時(shí)就用到class_addMethod
方法,它可以向類中動(dòng)態(tài)地添加方法,用以處理給定的選擇子。第三個(gè)參數(shù)為函數(shù)指針,指向待添加的方法。而最后一個(gè)參數(shù)則表示待添加方法的“類型編碼”(type encoding)。在本例中,編碼開頭的字符表示方法的返回值類型,后續(xù)字符則表示其所接受的各個(gè)參數(shù)。
EOCAutoDictionary的用法很簡(jiǎn)單:
EOCAutoDictionary *dict = [EOCAutoDictionary new];
dict.date = [NSDate dateWithTimeIntervalSincel970:475372800];
NSLog(@"dict.date = %@",dict.date);
// Output: diet.date = 1985-01-24 00:00:00 +0000
其他屬性的訪問(wèn)方式與date類似,要想添加新屬性,只需來(lái)定義,并將其聲明為@dynamic即可。在iOS的CoreAnimation框架中,CALayer類就用了與本例相似的實(shí)現(xiàn)方式,這使得CALayer成為兼容于“鍵值編碼的”(key-value-coding-compliant:除了使用存取方法和“點(diǎn)語(yǔ)法”之外,還可以用字符串做鍵,通過(guò)valueForKey:
與setValue:forKey:
這種形式來(lái)訪問(wèn)屬性) 容器類, 也就等于說(shuō),能夠向里面隨意添加屬性,然后以鍵值對(duì)的形式來(lái)訪問(wèn)。于是,開發(fā)者就可以向其中新增自定義的屬性了,這些屬性值的存儲(chǔ)工作由基類直接負(fù)責(zé),我們只需在CALayer 的子類中定義新屬性即可。
要點(diǎn):
- 若對(duì)象無(wú)法響應(yīng)某個(gè)選擇子,則進(jìn)入消息轉(zhuǎn)發(fā)流程。
- 通過(guò)運(yùn)行期的動(dòng)態(tài)方法解析功能,我們可以在需要用到某個(gè)方法時(shí)再將其加入類中。
- 對(duì)象可以把其無(wú)法解讀的某些選擇子轉(zhuǎn)交給其他對(duì)象來(lái)處理。
- 經(jīng)過(guò)上述兩步之后,如果還是沒(méi)辦法處理選擇子,那就啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制。
第13條:用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”
上面講了:Objective-C對(duì)象收到消息之后,方法需要在運(yùn)行期才能解析出來(lái)。那么與給定的選擇子名稱相對(duì)應(yīng)的方法也可以在運(yùn)行期改變。若能善用此特性,則可發(fā)揮出巨大優(yōu)勢(shì),因?yàn)槲覀兗炔恍枰创a,也不需要通過(guò)繼承子類來(lái)覆寫方法就能改變這個(gè)類本身的功能。這樣一來(lái),新功能將在本類的所有實(shí)例中生效,而不是僅限于覆寫了相關(guān)方法的那些子類實(shí)例。此方案經(jīng)常稱為 “方法調(diào)配”(method swizzling) 。
類的方法列表會(huì)把選擇子的名稱映射到相關(guān)的方法實(shí)現(xiàn)之上,使得“動(dòng)態(tài)消息派發(fā)系統(tǒng)” 能夠據(jù)此找到應(yīng)該調(diào)用的方法。這些方法均以函數(shù)指針的形式來(lái)表示,這種指針叫做IMP, 其原型如下:
id (*IMP) (id, SEL,…)
NSString 類可以響應(yīng)lowercaseString、uppercaseString、capitalizedString等選擇子。這張映射表中的每個(gè)選擇子都映射到了不同的IMP之上。
Objective-C運(yùn)行期系統(tǒng)提供的幾個(gè)方法都能夠用來(lái)操作這張表。開發(fā)者可以向其中新增選擇子,也可以改變某選擇子所對(duì)應(yīng)的方法實(shí)現(xiàn),還可以交換兩個(gè)選擇子所映射到的指針。 經(jīng)過(guò)幾次操作之后,類的方法表就會(huì)變成下圖這個(gè)樣子。
在新的映射表中,多了一個(gè)名為newSelector的選擇子,capitalizedString的實(shí)現(xiàn)也變了, 而lowercaseString與uppercaseString的實(shí)現(xiàn)則互換了。上述修改均無(wú)須編寫子類,只要修改了“方法表”的布局,就會(huì)反映到程序中所有的NSString實(shí)例之上。
在實(shí)際應(yīng)用中,直接交換兩個(gè)方法實(shí)現(xiàn)的意義并不大。我們一般使用該手段來(lái)為既有的方法實(shí)現(xiàn)增添新功能。 比方說(shuō),想要在調(diào)用lowercaseString時(shí)記錄某些信息,這時(shí)就可以通過(guò)交換方法實(shí)現(xiàn)來(lái)達(dá)成此目標(biāo)。我們新編寫一個(gè)方法,在此方法中實(shí)現(xiàn)所需的附加功能,并調(diào)用原有實(shí)現(xiàn)。
// .h 新方法可以添加至NSString的一個(gè)“分類”(category)中:
@interface NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end
//.m 新方法的實(shí)現(xiàn)代碼可以這樣寫:
@impleinentation NSString (EOCMyAdditions)
+ (void)load {
//方法實(shí)現(xiàn)則可通過(guò)下列函數(shù)獲得:
Method originalMethod = class_getInstanceMethod([NSString class],@selector(lowercaseString)〉;
Method swappedMethod = class_getInstanceMethod([NSString class],@selector(eoc_myLowercaseString));
//交換方法實(shí)現(xiàn)
method_exchangeImplementations(originalMethod,swappedMethod);
}
- (NSString*)eoc_myLowercaseString {
NSString *lowercase = [self eoc_myLowercaseString];
NSLog (@"%@ => %@", self, lowercase);
return lowercase;
)
@end
從現(xiàn)在開始,如果在NSString實(shí)例上調(diào)用lowercaseString,那么執(zhí)行的將是uppercaseString的原有實(shí)現(xiàn),反之亦然。
上述新方法將與原有的lowercaseString方法互換,交換之后的方法表如圖:
這段代碼看上去好像會(huì)陷入遞歸調(diào)用的死循環(huán),不過(guò)大家要記住,此方法是準(zhǔn)備和lowercaseString方法互換的。所以,在運(yùn)行期,eoc_myLowercaseString選擇子實(shí)際上對(duì)應(yīng)于原有的lowercaseString方法實(shí)現(xiàn)。
通過(guò)此方案,開發(fā)者可以為那些“完全不知道其具體實(shí)現(xiàn)的"(completely opaque, “完全不透明的”)黑盒方法增加日志記錄功能,這非常有助于程序調(diào)試。然而,此做法只在調(diào)試程序時(shí)有用。很少有人在調(diào)試程序之外的場(chǎng)合用上述“方法調(diào)配技術(shù)”來(lái)永久改動(dòng)某個(gè)類的功能。不能僅僅因?yàn)镺bjective-C語(yǔ)言里有這個(gè)特性就一定要用它。若是濫用,反而會(huì)令代碼變得不易讀懂且難于維護(hù)。
要點(diǎn):
- 在運(yùn)行期,可以向類中新增或替換選擇子所對(duì)應(yīng)的方法實(shí)現(xiàn)。
- 使用另一份實(shí)現(xiàn)來(lái)替換原有的方法實(shí)現(xiàn),這道工序叫做“方法調(diào)配”,開發(fā)者常用此 技術(shù)向原有實(shí)現(xiàn)中添加新功能。
- 一般來(lái)說(shuō),只有調(diào)試程序的時(shí)候才需要在運(yùn)行期修改方法實(shí)現(xiàn),這種做法不宜濫用。
第14條:理解“類對(duì)象”的用意
Objective-C實(shí)際上是一門極其動(dòng)態(tài)的語(yǔ)言。第11條講解了運(yùn)行期系統(tǒng)如何査找并調(diào)用某方法的實(shí)現(xiàn)代碼,第12條則講述了消息轉(zhuǎn)發(fā)的原理:如果類無(wú)法立即響應(yīng)某個(gè)選擇子, 那么就會(huì)啟動(dòng)消息轉(zhuǎn)發(fā)流程。然而,消息的接收者究竟是何物?是對(duì)象本身嗎?運(yùn)行期系統(tǒng)如何知道某個(gè)對(duì)象的類型呢?對(duì)象類型并非在編譯期就綁定好了,而是要在運(yùn)行期査找。而且,還有個(gè)特殊的類型叫做id,它能指代任意的Objective-C對(duì)象類型。一般情況下,應(yīng)該指明消息接收者的具體類型,這樣的話,如果向其發(fā)送了無(wú)法解讀的消息,那么編譯器就會(huì)產(chǎn)生警告信息。而類型為id的對(duì)象則不然,編譯器假定它能響應(yīng)所有消息。
如果看過(guò)第12條,你就會(huì)明白,編譯器無(wú)確定某類型對(duì)象到底能解讀多少種選擇子, 因?yàn)檫\(yùn)行期還可向其中動(dòng)態(tài)新增。然而,即便使用了動(dòng)態(tài)新增技術(shù),編譯器也覺(jué)得應(yīng)該能在某個(gè)頭文件中找到方法原型的定義,據(jù)此可了解完整的“方法簽名"(method signature),并生成派發(fā)消息所需的正確代碼。
“在運(yùn)行期檢視對(duì)象類型”這一操作也叫做“類型信息査詢”(introspection, “內(nèi)省”),這個(gè)強(qiáng)大而有用的特性內(nèi)置于Foundation框架的NSObject協(xié)議里,凡是由公共根類(common root class,即NSObject與NSProxy)繼承而來(lái)的對(duì)象都要遵從此協(xié)議。在程序中不要直接比較對(duì)象所屬的類,明智的做法是調(diào)用“類型信息査詢方法”,其原因筆者稍后解釋。不過(guò)在介紹類型信息査詢技術(shù)之前,我們先講一些基礎(chǔ)知識(shí),看看Objective-C對(duì)象的本質(zhì)是什么。
每個(gè)Objective-C對(duì)象實(shí)例都是指向某塊內(nèi)存數(shù)據(jù)的指針(???)。所以在聲明變量時(shí),類型后面要跟一個(gè)字符:
NSString *pointerVariable = @"Some string";
編過(guò)C語(yǔ)言程序的人都知道這是什么意思。對(duì)于沒(méi)寫過(guò)C語(yǔ)言的程序員來(lái)說(shuō), pointerVariable可以理解成存放內(nèi)存地址的變量,而NSString自身的數(shù)據(jù)就存于那個(gè)地址中。 因此可以說(shuō),該變量“指向”(point to) NSString實(shí)例。所有Objective-C對(duì)象都是如此,若是想把對(duì)象所需的內(nèi)存分配在棧上,編譯器則會(huì)報(bào)錯(cuò):
String stackVariable = @"Some string";
"error: interface type cannot be statically allocated
對(duì)于通用的對(duì)象類型id,由于其本身已經(jīng)是指針了,所以我們能夠這樣寫:
id genericTypedString = @"Some string";
上面這種定義方式與用NSString *
來(lái)定義相比,其語(yǔ)法意義相同。唯一區(qū)別在于,如果聲明時(shí)指定了具體類型,那么在該類實(shí)例上調(diào)用其所沒(méi)有的方法時(shí),編譯器會(huì)探知此情況,并發(fā)出警告信息。
描述Objective-C對(duì)象所用的數(shù)據(jù)結(jié)構(gòu)定義在運(yùn)行期程序庫(kù)的頭文件里,id類型本身也在定義在這里:
typedef struct objc_object {
Class isa;
} *id;
由此可見,每個(gè)對(duì)象結(jié)構(gòu)體的首個(gè)成員是Class類的變量。該變量定義了對(duì)象所屬的類, 通常稱為isa
指針。例如,剛才的例子中所用的對(duì)象“是一個(gè)”(isa) NSString,所以其“isa”指針就指向NSString。
Class對(duì)象也定義在運(yùn)行期程序庫(kù)的頭文件中:
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list *methodLists;
struct objc_cache *cache;
struct objc_protocol list *protocols;
);
此結(jié)構(gòu)體存放類的“元數(shù)據(jù)"(metadata),例如類的實(shí)例實(shí)現(xiàn)了幾個(gè)方法,具備多少個(gè)實(shí)例變量等信息。此結(jié)構(gòu)體的首個(gè)變量也是isa指針,這說(shuō)明Class本身亦為Objective-C對(duì)象。 結(jié)構(gòu)體里還有個(gè)變量叫做superclass,它定義了本類的超類。類對(duì)象所屬的類型(也就是isa 指針?biāo)赶虻念愋停┦橇硗庖粋€(gè)類,叫做“元類"(metadass),用來(lái)表述類對(duì)象本身所具備的元數(shù)據(jù)。“類方法”就定義于此處,因?yàn)檫@些方法可以理解成類對(duì)象的實(shí)例方法。每個(gè)類僅有一個(gè)“類對(duì)象”,而每個(gè)“類對(duì)象”僅有一個(gè)與之相關(guān)的“元類”。
假設(shè)有個(gè)名為SomeClass的子類從NSObject中繼承而來(lái),則其繼承體系如圖
superclass指針確立了繼承關(guān)系,而isa指針描述了實(shí)例所屬的類。通過(guò)這張布局關(guān)圖即可執(zhí)行“類型信息査詢”。我們可以査出對(duì)象是否能響應(yīng)某個(gè)選擇子,是否遵從某項(xiàng)協(xié)議,并且能看出此對(duì)象位于“類繼承體系”(class hierarchy)的哪一部分。
在類繼承體系中查詢類型信息
可以用類型信息査詢方法來(lái)檢視類繼承體系。isMemberOfClass:
能夠判斷出對(duì)象是否為某個(gè)特定類的實(shí)例,而isKindOfClass:
則能夠判斷出對(duì)象是否為某類或其派生類的實(shí)例。例如:
NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass: [NSDictionary class] ] ; //<NO
[dict isMemberOfClass:[NSMutableDictionary class]];//< YES
[dict isKindOfClass: [NSDictionary class]];//< YES
[dict isKindOfClass:[NSArray class]]; //< NO
像這樣的類型信息査詢方法使用isa指針獲取對(duì)象所屬的類,然后通過(guò)superclass指針在繼承體系中游走。由于對(duì)象是動(dòng)態(tài)的,所以此特性顯得極為重要。Objective-C與你可能熟悉的其他語(yǔ)言不同,在此語(yǔ)言中,必須査詢類型信息,方能完全了解對(duì)象的真實(shí)類型。
由于Objective-C使用“動(dòng)態(tài)類型系統(tǒng)"(dynamic typing),所以用于査詢對(duì)象所屬類的類型信息査詢功能非常有用。從collection中獲取對(duì)象時(shí),通常會(huì)査詢類型信息,這些對(duì)象不是“強(qiáng)類型的”(strongly typed),把它們從collection中取出來(lái)時(shí),其類型通常是id。如果想知道具體類型,那就可以使用類型信息査詢方法。
也可以用比較類對(duì)象是否等同的辦法來(lái)做。若是如此,那就要使用==操作符,而不要使用比較Objective-C對(duì)象時(shí)常用的“isEqual:”方法(參見第8條)。原因在于,類對(duì)象是 “單例”(singleton),在應(yīng)用程序范圍內(nèi),每個(gè)類的Class僅有一個(gè)實(shí)例。也就是說(shuō),另外一種可以精確判斷出對(duì)象是否為某類實(shí)例的辦法是:
id object = /* ??? */;
if ([object class] == [EOCSomeClass class]) {
//'object' is an instance of EOCSomeClass
)
即便能這樣做,我們也應(yīng)該盡景使用類型信息査詢方法,而不應(yīng)該直接比較兩個(gè)類對(duì)象是否等同,因?yàn)榍罢呖梢哉_處理那些使用了消息傳遞機(jī)制(參見第12條)的對(duì)象。比方說(shuō),某個(gè)對(duì)象可能會(huì)把其收到的所有選擇子都轉(zhuǎn)發(fā)給另外一個(gè)對(duì)象。這樣的對(duì)象叫做“代理” (proxy),此種對(duì)象均以NSProxy為根類。
通常情況下,如果在此種代理對(duì)象上調(diào)用class方法,那么返回的是代理對(duì)象本身(此類是NSProxy的子類),而非接受的代理的對(duì)象所屬的類。然而,若是改用isKindOfClass:
這樣的類型信息査詢方法,那么代理對(duì)象就會(huì)把這條消息轉(zhuǎn)給“接受代理的對(duì)象”(proxied object)。也就是說(shuō),這條消息的返回值與直接在接受代理的對(duì)象上面査詢其類型所得的結(jié)果相同。因此,這樣?xùn)顺鰜?lái)的類對(duì)象與通過(guò)class方法所返回的那個(gè)類對(duì)象不同,class方法所返回的類表示發(fā)起代理的對(duì)象,而非接受代理的對(duì)象。
要點(diǎn):
- 每個(gè)實(shí)例都有一個(gè)指向Class對(duì)象的指針,用以表明其類型,而這些Class對(duì)象則構(gòu)成了類的繼承體系。
- 如果對(duì)象類型無(wú)法在編譯期確定,那么就應(yīng)該使用類型信息査詢方法來(lái)探知。
- 盡量使用類型信息査詢方法來(lái)確定對(duì)象類型,而不要直接比較類對(duì)象,因?yàn)槟承?duì)象可能實(shí)現(xiàn)了消息轉(zhuǎn)發(fā)功能。
第三章 接口與API設(shè)計(jì)
第15條:用前綴避免命名空間沖突
Objective-C沒(méi)有其他語(yǔ)言那種內(nèi)置的命名空間(namespace)機(jī)制。鑒于此,我們?cè)谄鹈麜r(shí)要設(shè)法避免潛在的命名沖突,否則很容易就重名了。如果發(fā)生命名沖突(naming dash), 那么應(yīng)用程序的鏈接過(guò)程就會(huì)出錯(cuò),因?yàn)槠渲谐霈F(xiàn)了重復(fù)符號(hào):
duplicate symbol _OBJC_METACLASS_$_EOCTheClass in: build/something.o build/something_else.o
duplicate symbol _OBJC_CLASS_$—EOCTheClass in: build/something.o build/something_else.o
避免此問(wèn)題的唯一辦法就是變相實(shí)現(xiàn)命名空間:為所有名稱都加上適當(dāng)前綴。所選前綴可以是與公司、應(yīng)用程序或二者皆有關(guān)聯(lián)之名。Apple宣稱其保留使用所有“兩字母前綴” (two-letter prefix)的權(quán)利,所以你自己選用的前綴應(yīng)該是三個(gè)字母的。
不僅是類名,還有以下:
- 那么一定要給“分類” (category)及“分類”中的方法加上前綴(第25條解釋了這么做的原因)。
- 類的實(shí)現(xiàn)文件中所用的純C函數(shù)及全局變量,這個(gè)問(wèn)題必須要注意。因?yàn)樵诰幾g好的目標(biāo)文件中,這些名稱是要算作“頂級(jí)符號(hào)”(top-level symbol)的。
這么做的好處:若此符號(hào)出現(xiàn)在棧回溯信息中,則很容易就能判明問(wèn)題源自哪塊代碼。
要點(diǎn):
- 選擇與你的公司、應(yīng)用程序或二者皆有關(guān)聯(lián)之名稱作為類名的前綴,并在所有代碼中均使用這一前綴。
- 若自己所開發(fā)的程序庫(kù)中用到了第三方庫(kù),則應(yīng)為其中的名稱加上前綴。
第16條: 提供“全能初始化方法”
為對(duì)象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(designated initializer) 。令其他初始化方法都來(lái)調(diào)用它。只有在全能初始化方法中,才會(huì)存儲(chǔ)內(nèi)部數(shù)據(jù)。這樣的話,當(dāng)?shù)讓訑?shù)據(jù)存儲(chǔ)機(jī)制改變時(shí),只需修改此方法的代碼就好,無(wú)須改動(dòng)其他初始化方法。
每個(gè)子類的全能初始化方法都應(yīng)該調(diào)用其超類的對(duì)應(yīng)方法,并逐層向上。
要點(diǎn):
- 在類中提供一個(gè)全能初始化方法,并于文檔里指明。其他初始化方法均應(yīng)調(diào)用此方法。
- 若全能初始化方法與超類不同,則需覆寫超類中的對(duì)應(yīng)方法。
- 如果超類的初始化方法不適用于子類,那么應(yīng)該覆寫這個(gè)超類方法,并在其中拋出異常。
第17條:實(shí)現(xiàn)description方法
調(diào)試程序時(shí),經(jīng)常需要打印并查看對(duì)象信息。構(gòu)建需要打印到日志的字符串時(shí),object對(duì)象會(huì)收到description消息,該方法所返回的描述信息將取代“格式字符串”(format string:在這里指%@)。
當(dāng)自定義對(duì)象,并想輸出更為有用的信息也很簡(jiǎn)單,只需覆寫description方法并將描述此對(duì)象的字符串返回即可。
NSObject協(xié)議中還有個(gè)方法要注意,那就是debugDescription,此方法的用意與description非常相似。二者區(qū)別在于,debugDescription方法是開發(fā)者在調(diào)試器(debugger) 中以控制臺(tái)命令(比如LLDB的po命令
)打印對(duì)象時(shí)才調(diào)用的。在NSObject類的默認(rèn)實(shí)現(xiàn)中,此方法只是直接調(diào)用了description 。
要點(diǎn):
- 實(shí)現(xiàn)description方法返回一個(gè)有意義的字符串,用以描述該實(shí)例。
- 若想在調(diào)試時(shí)打印出更詳盡的對(duì)象描述信息,則應(yīng)實(shí)現(xiàn)debugDescription方法。
第18條:盡量使用不可變對(duì)象
設(shè)計(jì)類的時(shí)候,應(yīng)充分運(yùn)用屬性來(lái)封裝數(shù)據(jù)(參見第6條)。默認(rèn)情況下,屬性是“既可讀又可寫的"(read-write),這樣設(shè)計(jì)出來(lái)的類都是“可變的”(mutable)。具體到編程實(shí)踐中,則應(yīng)該盡量把對(duì)外公布出來(lái)的屬性設(shè)為只讀,而且只在確有必要時(shí)才將屬性對(duì)外公布。
當(dāng)然,如果該屬性是nonatomic的,那么這樣做可能會(huì)產(chǎn)生“競(jìng)爭(zhēng)條件”(racecondition)。在對(duì)象內(nèi)部寫入某屬性時(shí),對(duì)象外的觀察者也許正讀取該屬性。若想避免此問(wèn)題,我們可以在必要時(shí)通過(guò)“派發(fā)隊(duì)列"(dispatch queue, 參見第41條)等手段,將(包括對(duì)象內(nèi)部的)所有數(shù)據(jù)存取操作都設(shè)為同步操作。
kvc和類型查詢:
- 現(xiàn)在,只能于實(shí)現(xiàn)代碼內(nèi)部設(shè)置這些屬性值了。其實(shí)更準(zhǔn)確地說(shuō), 在對(duì)象外部,仍然能通過(guò)“鍵值編碼”(Key-Value Coding,KVC)技術(shù)設(shè)置這些屬性值,比如說(shuō):
[pointOfInterest setValue:@"abc forKey:@"identifier"];
這樣做可以改動(dòng)identifier屬性,因?yàn)镵VC會(huì)在類里査找setIdentifier:
方法,并借此修改此屬性。不過(guò),這樣做等于違規(guī)地繞過(guò)了本類所提供的API。 - 有些“愛用蠻力的”(brutal)程序員甚至不通過(guò)“設(shè)置方法”,而是直接用類型信息查詢功能査出屬性所對(duì)應(yīng)的實(shí)例變量在內(nèi)存布局中的偏移量,以此來(lái)人為設(shè)置這個(gè)實(shí)例變量的值。這樣做比繞過(guò)本類的公共API還要不合規(guī)范
對(duì)于集合:
- 如果把可變對(duì)象(mutable object)放入collection之后又修改其內(nèi)容,那么很容易就會(huì)破壞set的內(nèi)部數(shù)據(jù)結(jié)構(gòu),使其失去固有的語(yǔ)義。因此,筆者建議大家盡量減少對(duì)象中的可變內(nèi)容。
- 對(duì)象里表示各種collection的那些屬性究競(jìng)應(yīng)該設(shè)成可變的,還是不可變的。在這種情況下,通常應(yīng)該提供一個(gè)readonly屬性供外界使用,該屬性將返回不可變的set, 而此set則是內(nèi)部那個(gè)可變set的一份拷貝。
要點(diǎn):
- 盡量創(chuàng)建不可變的對(duì)象。
- 若某屬性僅可于對(duì)象內(nèi)部修改,則在“class-continuation分類”中將其由readonly屬性擴(kuò)展為readwrite屬性,屬性的其他特質(zhì)必須保持不變。
- 不要把可變的collection作為屬性公開,而應(yīng)提供相關(guān)方法,以此修改對(duì)象中的可變collection。
第19條:使用清晰而協(xié)調(diào)的命名方式
方法與變量名使用了“駝峰式大小寫命名法"(camel casing)-以小寫字母開頭,其后每個(gè)單詞首字母大寫。類名也用駝峰命名法,不過(guò)其首字母要大寫,而且前面通常還有兩三個(gè)前綴字母(參見第15條)。在編寫Objective-C代碼時(shí),大家一般都使用這種命名方式。
給方法命名時(shí)的注意事項(xiàng)可總結(jié)成下面幾條規(guī)則:
- 如果方法的返回值是新創(chuàng)建的,那么方法名的首個(gè)詞應(yīng)是返回值的類型,除非前面還有修飾語(yǔ),例如localizedString。屬性的存取方法不遵循這種命名方式,因?yàn)橐话阏J(rèn)為這些方法不會(huì)創(chuàng)建新對(duì)象,即便有時(shí)返回內(nèi)部對(duì)象的一份拷貝,我們也認(rèn)為那相當(dāng)于原有的對(duì)象。這些存取方法應(yīng)該按照其所對(duì)應(yīng)的屬性來(lái)命名。
- 應(yīng)該把表示參數(shù)類型的名詞放在參數(shù)前面。
- 如果方法要在當(dāng)前對(duì)象上執(zhí)行操作,那么就應(yīng)該包含動(dòng)詞;若執(zhí)行操作時(shí)還需要參數(shù), 則應(yīng)該在動(dòng)詞后面加上一個(gè)或多個(gè)名詞。
- 不要使用str這種簡(jiǎn)稱,應(yīng)該用string這樣的全稱。
- Boolean屬性應(yīng)加is前綴。如果某方法返回非屬性的Boolean值,那么應(yīng)該根據(jù)其功能,選用has或is當(dāng)前綴。
- 將get這個(gè)前綴留給那些借由“輸出參數(shù)”來(lái)保存返回值的方法,比如說(shuō),把返回值填充到“C語(yǔ)言式數(shù)組”(C-style array)里的那種方法就可以使用這個(gè)詞做前綴。
類與協(xié)議的命名
應(yīng)該為類與協(xié)議的名稱加上前綴,以避免命名空間沖突(參見第15條),如果要從其他框架中繼承子類,那么務(wù)必遵循其命名慣例。比方說(shuō),要從UlView類中繼承自定義的子類, 那么類名末尾的詞必須是View。同理,若要?jiǎng)?chuàng)建自定義的委托協(xié)議,則其名稱中應(yīng)該包含委托發(fā)起方的名稱,后面再跟上Delegate—詞。
要點(diǎn):
- 起名時(shí)應(yīng)遵從標(biāo)準(zhǔn)的Objective-C命名規(guī)范,這樣創(chuàng)建出來(lái)的接口更容易為開發(fā)者所 理解。
- 方法名要言簡(jiǎn)意賅,從左至右讀起來(lái)要像個(gè)日常用語(yǔ)中的句子才好。
- 方法名里不要使用縮略后的類型名稱。
- 給方法起名時(shí)的第一要?jiǎng)?wù)就是確保其風(fēng)格與你自己的代碼或所要集成的框架相符。
第20條:為私有方法名加前綴
具體使用何種前綴可根據(jù)個(gè)人喜好來(lái)定,其中最好包含字母P與下劃線。比如
- (void)p_add {
//...
}
如果寫過(guò)C++或Java代碼,你可能就會(huì)問(wèn)了:為什么要這樣做呢?直接把方法聲明成私有的不就好了嗎? Objective-C語(yǔ)言沒(méi)辦法將方法標(biāo)為私有。每個(gè)對(duì)象都可以響應(yīng)任意消息(參見第12條),而且可在運(yùn)行期檢視某個(gè)對(duì)象所能直接響應(yīng)的消息(參見第14條)。根據(jù)給定的消息査出其對(duì)應(yīng)的方法,這一工作要在運(yùn)行期才能完成(參見第11條),所以 Objective-C中沒(méi)有那種約束方法調(diào)用的機(jī)制用以限定誰(shuí)能調(diào)用此方法、能在哪個(gè)對(duì)象上調(diào)用此方法以及何時(shí)能調(diào)用此方法。開發(fā)者會(huì)在命名慣例中體現(xiàn)出“私有方法”等語(yǔ)義。必須用心領(lǐng)悟Objective-C語(yǔ)言這種強(qiáng)大的動(dòng)態(tài)特性。
要點(diǎn):
- 給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區(qū)分開。
- 不要單用一個(gè)下劃線做私有方法的前綴,因?yàn)檫@種做法是預(yù)留給蘋果公司用的
第21條:理解Objective-C錯(cuò)誤模型
當(dāng)前很多種編程語(yǔ)言都有“異常”(exception)機(jī)制,Objective-C也不例外。寫過(guò)Java 代碼的程序員應(yīng)該很習(xí)慣于用異常來(lái)處理錯(cuò)誤。如果你也是這么使用異常的,那現(xiàn)在就把它忘了吧,我們得從頭學(xué)起。
首先要注意的是,“自動(dòng)引用計(jì)數(shù)”(Automatic ReferenceCounting, ARC,參見第30條)在默認(rèn)情況下不是“異常安全的"(exception safe)。具體來(lái)說(shuō),這意味著:如果拋出異常,那么本應(yīng)在作用域末尾釋放的對(duì)象現(xiàn)在卻不會(huì)自動(dòng)釋放了。如果想生成“異常安全”的代碼, 可以通過(guò)設(shè)置編譯器的標(biāo)志來(lái)實(shí)現(xiàn),不過(guò)這將引入一些額外代碼,在不拋出異常時(shí),也照樣要執(zhí)行這部分代碼。需要打開的編譯器標(biāo)志叫做-fobjc-arc-exceptions
。
既然異常只用于處理嚴(yán)重錯(cuò)誤(fatal error,致命錯(cuò)誤),那么對(duì)其他錯(cuò)誤怎么辦呢?在出現(xiàn)“不那么嚴(yán)重的錯(cuò)誤"(nonfatalerror,非致命錯(cuò)誤)時(shí),Objective-C語(yǔ)言所用的編程范式為: 令方法返回nil/0,或是使用NSError,以表明其中有錯(cuò)誤發(fā)生。
要點(diǎn):
- 只有發(fā)生了可使整個(gè)應(yīng)用程序崩潰的嚴(yán)重錯(cuò)誤時(shí),才應(yīng)使用異常。
- 在錯(cuò)誤不那么嚴(yán)重的情況下,可以指派“委托方法”(delegate method)來(lái)處理錯(cuò)誤,也可以把錯(cuò)誤信息放在NSError對(duì)象里,經(jīng)由“輸出參數(shù)”返回給調(diào)用者。
第22條:理解NSCopying協(xié)議
使用對(duì)象時(shí)經(jīng)常需要拷貝它。在Objective-C中,此操作通過(guò)copy方法完成。如果想令自己的類支持拷貝操作,那就要實(shí)現(xiàn)NSCopying協(xié)議,該協(xié)議只有一個(gè)方法:
- (id)copyWithZone:(NSZone *)zone
為何會(huì)出現(xiàn)NSZone呢?因?yàn)橐郧伴_發(fā)程序時(shí),會(huì)據(jù)此把內(nèi)存分成不同的“區(qū)”(zone), 而對(duì)象會(huì)創(chuàng)建在某個(gè)區(qū)里面。現(xiàn)在不用了,每個(gè)程序只有一個(gè)區(qū):“默認(rèn)區(qū)”(default zone)。 所以說(shuō),盡管必須實(shí)現(xiàn)這個(gè)方法,但是你不必?fù)?dān)心其中的zone參數(shù)。
copy->_friends = [_friends mutableCopy];
這次所實(shí)現(xiàn)的方法比原來(lái)多了一些代碼,它把本對(duì)象的_friends實(shí)例變量復(fù)制了一份, 令copy對(duì)象的_friends實(shí)例變量指向這個(gè)復(fù)制過(guò)的set。(注意:這里使用了->語(yǔ)法,因?yàn)?/em> friends并非屬性,只是個(gè)在內(nèi)部使用的實(shí)例變量。其實(shí)也可以聲明一個(gè)屬性來(lái)表示它,不過(guò)由于該變量不會(huì)在本類之外使用,所以那么做沒(méi)必要)
NSMutableCopying的協(xié)議。該協(xié)議與 NSCopying類似,也只定義了一個(gè)方法,然而方法名不同:
- (id)mutableCopyWithZone:(NSZone*)zone
在編寫拷貝方法時(shí),還要決定一個(gè)問(wèn)題,就是應(yīng)該執(zhí)行“深拷貝”(deep copy)還是“淺拷貝”(shallowcopy)。深拷貝的意思就是:在拷貝對(duì)象自身時(shí),將其底層數(shù)據(jù)也一并復(fù)制過(guò)去。Foundation框架中的所有collection類在默認(rèn)情況下都執(zhí)行淺拷貝,也就是說(shuō),只拷貝容器對(duì)象本身,而不復(fù)制其中數(shù)據(jù)。這樣做的主要原因在于,容器內(nèi)的對(duì)象未必都能拷貝,而且調(diào)用者也未必想在拷貝容器時(shí)一并拷貝其中的每個(gè)對(duì)象。
以NSSet為例,該類提供了下面這個(gè)初始化方法,用以執(zhí)行深拷貝:
-(id)initWithSet:(NSArray^)array copyltems:(BOOL)copyltems
若copyltem參數(shù)設(shè)為YES,則該方法會(huì)向數(shù)組中的每個(gè)元素發(fā)送copy消息,用拷貝好的元素創(chuàng)建新的set,并將其返回給調(diào)用者。
要點(diǎn):
- 若想令自己所寫的對(duì)象具有拷貝功能,需實(shí)現(xiàn)NSCopying協(xié)議。
- 如果自定義的對(duì)象分為可變版本與不可變版本,那么就要同時(shí)實(shí)現(xiàn)NSCopying與 NSMutableCopying 協(xié)議。
- 復(fù)制對(duì)象時(shí)需決定采用淺拷貝還是深拷貝,一般情況下應(yīng)該盡量執(zhí)行淺拷貝。
第四章 協(xié)議與分類
Objective-C語(yǔ)言有一項(xiàng)特性叫做“協(xié)議”(protocol),它與Java的“接口"(interface)類似,Objective-C不支持多重繼承,因而我們把某個(gè)類應(yīng)該實(shí)現(xiàn)的一系列方法定義在協(xié)議里面。 協(xié)議最為常見的用途是實(shí)現(xiàn)委托模式(參見第23條),不過(guò)也有其他用法。
“分類”(Category)也是Objective-C的一項(xiàng)重要語(yǔ)言特性。利用分類機(jī)制,我們無(wú)須繼承子類即可直接為當(dāng)前類添加方法,而在其他編程語(yǔ)言中,則需通過(guò)繼承子類來(lái)實(shí)現(xiàn)。由于Objective-C運(yùn)行期系統(tǒng)是髙度動(dòng)態(tài)的,所以才能支持這一特性,然而,其中也隱藏著一些陷阱。
第23條:通過(guò)委托與數(shù)據(jù)源協(xié)議進(jìn)行對(duì)象間通信
對(duì)象之間經(jīng)常需要相互通信,而通信方式有很多種。Objective-C開發(fā)者廣泛使用一種名叫“委托模式"(Delegatepattem)的編程設(shè)計(jì)模式來(lái)實(shí)現(xiàn)對(duì)象間的通信,該模式的主旨是: 定義一套接口,某對(duì)象若想接受另一個(gè)對(duì)象的委托,則需遵從此接口,以便成為其“委托對(duì)象”(delegate)。而這“另一個(gè)對(duì)象”則可以給其委托對(duì)象回傳一些信息。
在Objective-C中,一般通過(guò)“協(xié)議”這項(xiàng)語(yǔ)言特性來(lái)實(shí)現(xiàn)此模式:
- 委托協(xié)議名通常是在相關(guān)類名后面加上
Delegate
—詞,整個(gè)類名采用“駝峰法”來(lái)寫。 - 有了這個(gè)協(xié)議之后,類就可以用一個(gè)屬性來(lái)存放其委托對(duì)象了。
- 實(shí)現(xiàn)委托對(duì)象的辦法是聲明某個(gè)類遵從委托協(xié)議(一般都是在“class-continuation分類”里聲明的)
- 把協(xié)議中想實(shí)現(xiàn)的那些方法在類里實(shí)現(xiàn)出來(lái)。
注意點(diǎn):
- 存放委托對(duì)象的屬性需定義成
weak
,而非strong
,因?yàn)閮烧咧g必須為“非擁有關(guān)系” (nonowning relationship)。通常情況下,扮演delegate的那個(gè)對(duì)象也要持有本對(duì)象。 - 如果要在委托對(duì)象上調(diào)用可選方法,那么必須提前使用類型信息査詢方法
respondsToSelector:
判斷這個(gè)委托對(duì)象能否響應(yīng)相關(guān)選擇子。
大家應(yīng)該很容易理解此模式為何叫做“委托模式”:因?yàn)閷?duì)象把應(yīng)對(duì)某個(gè)行為的責(zé)任委托給另外一個(gè)類了。也可以用協(xié)議定義一套接口,令某類經(jīng)由該接口獲取其所需的數(shù)據(jù)。委托模式的這一用法旨在向類提供數(shù)據(jù),故而又稱“數(shù)據(jù)源模式”(Data Source Pattern)。在此模式中,信息從數(shù)據(jù)源(Data Source)流向類(Class);而在常規(guī)的委托模式中,信息則從類流向受委托者 (Delegate)。
下圖演示這兩條信息流:在信息源模式中,信息從數(shù)據(jù)源流向類,而在普通的委托模式中,信息則從類流向受委托者
比方說(shuō),用戶界面框架中的“列表視圖”(list view)對(duì)象可能會(huì)通過(guò)數(shù)據(jù)源協(xié)議來(lái)獲取要在列表中顯示的數(shù)據(jù)。除了數(shù)據(jù)源之外,列表視圖還有一個(gè)受委托者,用于處理用戶與列表的交互操作。將數(shù)據(jù)源協(xié)議與委托協(xié)議分離,能使接口更加清晰,因?yàn)檫@兩部分的邏輯代碼也分開了。另外,“數(shù)據(jù)源”與“受委托者”可以是兩個(gè)不同的對(duì)象。然而一般情況下, 都用同一個(gè)對(duì)象來(lái)扮演這兩種角色。
要點(diǎn):
- 委托模式為對(duì)象提供了一套接口,使其可由此將相關(guān)事件告知其他對(duì)象。
- 將委托對(duì)象應(yīng)該支持的接口定義成協(xié)議,在協(xié)議中把可能需要處理的事件定義成方法。
- 當(dāng)某對(duì)象需要從另外一個(gè)對(duì)象中獲取數(shù)據(jù)時(shí),可以使用委托模式。這種情境下,該模式亦稱“數(shù)據(jù)源協(xié)議”(data source protocal)。
- 若有必要,可實(shí)現(xiàn)含有位段的結(jié)構(gòu)體,將委托對(duì)象是否能響應(yīng)相關(guān)協(xié)議方法這一信息緩存至其中。
第24條:將類的實(shí)現(xiàn)代碼分散到便于管理的數(shù)個(gè)分類之中
類中經(jīng)常容易填滿各種方法,而這些方法的代碼則全部堆在一個(gè)巨大的實(shí)現(xiàn)文件里。 在此情況下,可以通過(guò)Objective-C的“分類”機(jī)制,把類代碼按邏輯劃入幾個(gè)分區(qū)中。
現(xiàn)在,類的實(shí)現(xiàn)代碼按照方法分成了好幾個(gè)部分。所以說(shuō),這項(xiàng)語(yǔ)言特性當(dāng)然就叫做 “分類”啦。類的基本要素(諸如屬性與初始化方法等)都聲明在“主實(shí)現(xiàn)"(main implementation)里。執(zhí)行不同類型的操作所用的另外幾套方法則歸入各個(gè)分類中。
使用分類機(jī)制之后,依然可以把整個(gè)類都定義在一個(gè)接口文件中,并將其代碼寫在一個(gè)實(shí)現(xiàn)文件里。 此時(shí)可以把每個(gè)分類提取到各自的文件中去。如果想用分類中的方法,那么要記得在引入主類時(shí)一并引入分類的頭文件。以下是原因:
- 隨著分類數(shù)量增加,當(dāng)前這份實(shí)現(xiàn)文件很快就膨脹得無(wú)法管理了。
- 便于調(diào)試:對(duì)于某個(gè)分類中的所有方法來(lái)說(shuō),分類名稱都會(huì)出現(xiàn)在其符號(hào)中,在調(diào)試器的回溯信息中會(huì)出現(xiàn)。例如,“addFriend:”方法的“符號(hào)名”(symbol name):
- [EOCPerson(Friendship) addFriend:1];
要點(diǎn):
- 使用分類機(jī)制把類的實(shí)現(xiàn)代碼劃分成易于管理的小塊。
- 將應(yīng)該視為“私有”的方法歸入名叫Private的分類中,以隱藏實(shí)現(xiàn)細(xì)節(jié)。
第25條:總是為第三方類的分類名稱加前綴
分類機(jī)制通常用于向無(wú)源碼的既有類中新增功能。這個(gè)特性極為強(qiáng)大,但在使用時(shí)也很容易忽視其中可能產(chǎn)生的問(wèn)題。這個(gè)問(wèn)題在于:分類中的方法是直接添加在類里面的,它們就好比這個(gè)類中的固有方法。將分類方法加入類中這一操作是在運(yùn)行期系統(tǒng)加載分類時(shí)完成的。運(yùn)行期系統(tǒng)會(huì)把分類中所實(shí)現(xiàn)的每個(gè)方法都加入類的方法列表中。如果類中本來(lái)就有此方法,而分類又實(shí)現(xiàn)了一次,那么分類中的方法會(huì)覆蓋原來(lái)那一份實(shí)現(xiàn)代碼。實(shí)際上可能會(huì)發(fā)生很多次覆蓋,比如某個(gè)分類中的方法覆蓋了“主實(shí)現(xiàn)”中的相關(guān)方法,而另外一個(gè)分類中的方法又覆蓋了這個(gè)分類中的方法。多次覆蓋的結(jié)果以最后一個(gè)分類為準(zhǔn)。(注:不是覆蓋,是根據(jù)selector找到的第一個(gè)方法為準(zhǔn),其后不再遍歷查找后序的同名方法)
要解決此問(wèn)題,一般的做法是:以命名空間來(lái)區(qū)別各個(gè)分類的名稱與其中所定義的方法。Obiective-C中實(shí)現(xiàn)命名空間功能,只有一個(gè)辦法,就是給相關(guān)名稱都加上某個(gè)共用的前綴。
要點(diǎn):
- 向第三方類中添加分類時(shí),總應(yīng)給其名稱加上你專用的前綴。
- 向第三方類中添加分類時(shí),總應(yīng)給其中的方法名加上你專用的前綴。
第26條:勿在分類中聲明屬性
屬性是封裝數(shù)據(jù)的方式(參見第6條)。盡管從技術(shù)上說(shuō),分類里也可以聲明屬性,但這種做法還是要盡量避免。原因在于,除了 “class-continuation分類”(參見第27條)之外,其他分類都無(wú)法向類中新增實(shí)例變量,因此,它們無(wú)法把實(shí)現(xiàn)屬性所需的實(shí)例變量合成出來(lái)。
有兩種辦法可以在分類里聲明屬性:
- 把存取方法聲明為@dynamic, 也就是說(shuō),這些方法等到運(yùn)行期再提供,編譯器目前是看不見的。如果決定使用消息轉(zhuǎn)發(fā)機(jī)制(參見第12條)在運(yùn)行期攔截方法調(diào)用,并提供其實(shí)現(xiàn),那么或許可以采用這種做法。
- 關(guān)聯(lián)對(duì)象(參見第10條)能夠解決在分類中不能合成實(shí)例變量的問(wèn)題。但是在內(nèi)存管理問(wèn)題上容易出錯(cuò)。
正確做法是把所有屬性都定義在主接口里。類所封裝的全部數(shù)據(jù)都應(yīng)該定義在主接口中,這里是唯一能夠定義實(shí)例變量(也就是數(shù)據(jù))的地方。而屬性只是定義實(shí)例變量及相關(guān)存取方法所用的“語(yǔ)法糖”,所以也應(yīng)遵循同實(shí)例變量一樣的規(guī)則。至于分類機(jī)制,則應(yīng)將其理解為一種手段,目標(biāo)在于擴(kuò)展類的功能,而非封裝數(shù)據(jù)。
雖說(shuō)如此,但有時(shí)候只讀屬性還是可以在分類中使用的。由于獲取方法并不訪問(wèn)數(shù)據(jù),而且屬性也不需要由實(shí)例變量來(lái)實(shí)現(xiàn)。即便在這種情況下,也最好不要用屬性。屬性所要表達(dá)的意思是:類中有數(shù)據(jù)在支持著它。屬性是用來(lái)封裝數(shù)據(jù)的。某些情況下也可以直接聲明一個(gè)方法,用以獲取數(shù)據(jù)。
要點(diǎn):
- 把封裝數(shù)據(jù)所用的全部屬性都定義在主接口里。
- 在“dass-contimiation分類”之外的其他分類中,可以定義存取方法,但盡量不要定義屬性。
第27條:使用“class-continuation分類”隱藏實(shí)現(xiàn)細(xì)節(jié)
類中經(jīng)常會(huì)包含一些無(wú)須對(duì)外公布的方法及實(shí)例變量。其實(shí)這些內(nèi)容也可以對(duì)外公布,并且寫明其為私有,但是這樣會(huì)泄漏實(shí)現(xiàn)細(xì)節(jié)。Objective-C動(dòng)態(tài)消息系統(tǒng)(參見第11條)的工作方式?jīng)Q定了其不可能實(shí)現(xiàn)真正的私有方法或私有實(shí)例變量。然而,我們最好還是只把確實(shí)需要對(duì)外公布的那部分內(nèi)容公開,隱藏部分細(xì)節(jié)。那么,這個(gè)特殊的“class-continuation分類”就派上用場(chǎng)了。
“class-continuation分類”和普通的分類不同,它必須定義在其所接觸的那個(gè)類的實(shí)現(xiàn)文件里。其重要之處在于,這是唯一能聲明實(shí)例變量的分類,而且此分類沒(méi)有特定的實(shí)現(xiàn)文件,其中的方法都應(yīng)該定義在類的主實(shí)現(xiàn)文件里。與其他分類不同,“class-continuation分類”沒(méi)有名字。
為什么需要有這種分類呢?因?yàn)槠渲锌梢远x方法和實(shí)例變量。為什么能在其中定義方法和實(shí)例變量呢?只因有“穩(wěn)固的ABI”這一機(jī)制(第6條詳解了此機(jī)制),使得我們無(wú)須知道對(duì)象大小即可使用它。由于類的使用者不一定需要知道實(shí)例變量的內(nèi)存布局,所以,它們也就未必非得定義在公共接口中了。基于上述原因,我們可以像在類的實(shí)現(xiàn)文件里那樣,于 “ class-contimiatiori分類”中給類新增實(shí)例變量。
實(shí)例變量也可以定義在實(shí)現(xiàn)塊里,從語(yǔ)法上說(shuō),這與直接添加到“class-continuation 分類”等效,只是看個(gè)人喜好了。筆者喜歡將其添加在“dass-continuation分類”中。這些實(shí)例變量并非真的私有,因?yàn)樵谶\(yùn)行期總可以調(diào)用某些方法繞過(guò)此限制,不過(guò),從一般意義上來(lái)說(shuō),它們還是私有的。
最后還要講一種用法:比方說(shuō),EOCPerson遵從了名為EOCSecretDelegate的協(xié)議。聲明在公共接口里。你可能會(huì)說(shuō),只需要向前聲明EOCSecretDelegate協(xié)議就可以不引入它了(或者說(shuō),不引入定義該協(xié)議的頭文件了)。用下面這行向前聲明語(yǔ)句來(lái)取代#import指令:
@protocol EOCSecretDelegate;
但是這樣一來(lái),只要引入EOCPerson頭文件的地方,由于編譯器看不到協(xié)議的定義,所以無(wú)法得知其中所含的方法,于是編譯器會(huì)給出瞥告信息。
要點(diǎn):
- 通過(guò)“class-continuation分類”向類中新增實(shí)例變量。
- 如果某屬性在主接口中聲明為“只讀”,而類的內(nèi)部又要用設(shè)置方法修改此屬性,那 么就在“class-continuation分類”中將其擴(kuò)展為“可讀寫”。
- 把私有方法的原型聲明在“class-continuation分類”里面。
- 若想使類所遵循的協(xié)議不為人所知,則可于“class-continuation分類”中聲明。
第28條:通過(guò)協(xié)議提供匿名對(duì)象
協(xié)議定義了一系列方法,遵從此協(xié)議的對(duì)象應(yīng)該實(shí)現(xiàn)它們(如果這些方法不是可選的, 那么就必須實(shí)現(xiàn))。于是,我們可以用協(xié)議把自己所寫的API之中的實(shí)現(xiàn)細(xì)節(jié)隱藏起來(lái),將返回的對(duì)象設(shè)計(jì)為遵從此協(xié)議的純id類型。這樣的話,想要隱藏的類名就不會(huì)出現(xiàn)在API之中了。若是接口背后有多個(gè)不同的實(shí)現(xiàn)類,而你又不想指明具體使用哪個(gè)類,那么可以考慮用這個(gè)辦法——因?yàn)橛袝r(shí)候這些類可能會(huì)變,有時(shí)候它們又無(wú)法容納于標(biāo)準(zhǔn)的類繼承體系中,因而不能以某個(gè)公共基類來(lái)統(tǒng)一表示。
此概念經(jīng)常稱為“匿名對(duì)象"(anonymous object),這與其他語(yǔ)言中的“匿名對(duì)象”不同, 在那些語(yǔ)言中,該詞是指以內(nèi)聯(lián)形式所創(chuàng)建出來(lái)的無(wú)名類,而此詞在Objective-C中則不是這個(gè)意思。第23條解釋了委托與數(shù)據(jù)源對(duì)象,其中就曾用到這種匿名對(duì)象。例如,在定義 “受委托者”(delegate)這個(gè)屬性時(shí),可以這樣寫:
@property {nonatomic, weak) id <EOCDelegate> delegate;
由于該屬性的類型是id <EOCDelegate>
,所以實(shí)際上任何類的對(duì)象都能充當(dāng)這一屬性, 即便該類不繼承自NSObject也可以,只要遵循EOCDelegate協(xié)議就行。對(duì)于具備此屬性的類來(lái)說(shuō),delegate就是“匿名的”(ammymous)。如有需要,可在運(yùn)行期査出此對(duì)象所屬的類型(參見第14條)。然而這樣做不太好,因?yàn)橹付▽傩灶愋蜁r(shí)所寫的那個(gè)EOCDelegate契約已經(jīng)表明此對(duì)象的具體類型無(wú)關(guān)緊要了。
NSDictionary也能實(shí)際說(shuō)明這一概念。在字典中,鍵的標(biāo)準(zhǔn)內(nèi)存管理語(yǔ)義是“設(shè)置時(shí)拷貝”,而值的語(yǔ)義則是“設(shè)置時(shí)保留”。因此,在可變版本的字典中,設(shè)置鍵值對(duì)所用的方法的簽名是:
- (void)setObject:(id)object forKey:(id<NSCopying>)key;
表示鍵的那個(gè)參數(shù)其類型為id<NSCopying>
,作為參數(shù)值的對(duì)象,它可以是任何類型, 只要遵從NSCopying協(xié)議就好,這樣的話,就能向該對(duì)象發(fā)送拷貝消息了。這個(gè)key參數(shù)可以視為匿名對(duì)象。與delegate屬性一樣,字典也不關(guān)心key對(duì)象所屬的具 體類,而且它也決不應(yīng)該依賴于此。字典對(duì)象只要能確定它可以給此實(shí)例發(fā)送拷貝消息就行了。
有時(shí)對(duì)象類型并不重要,重要的是對(duì)象有沒(méi)有實(shí)現(xiàn)某些方法,在此情況下,也可以用這些“匿名類型”(anonymous type)來(lái)表達(dá)這一概念。即便實(shí)現(xiàn)代碼總是使用固定的類,你可能還是會(huì)把它寫成遵從某協(xié)議的匿名類型,以表示類型在此處并不重要。
要點(diǎn):
- 協(xié)議可在某種程度上提供匿名類型。具體的對(duì)象類型可以淡化成遵從某協(xié)議的id類型,協(xié)議里規(guī)定了對(duì)象所應(yīng)實(shí)現(xiàn)的方法。
- 使用匿名對(duì)象來(lái)隱藏類型名稱(或類名)。
- 如果具體類型不重要,重要的是對(duì)象能夠響應(yīng)(定義在協(xié)議里的)特定方法,那么可使用匿名對(duì)象來(lái)表示。