iOS開發(fā)讀書筆記:Effective Objective-C 2.0 52個(gè)有效方法 - 篇2/4

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)處理:

  1. 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)體。
  2. 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é)驚訝的奇怪狀況。
  3. 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):

  1. 消息由接收者、選擇子及參數(shù)構(gòu)成。給某對(duì)象潑送消息"(invoke a message:也是“調(diào)用”的意思,此處為了與“call”相區(qū)隔,將其臨時(shí)譯為“發(fā)送”,也可理解為“激發(fā)”、 “觸發(fā))”也就相當(dāng)于在該對(duì)象上“調(diào)用方法”(call a method)。
  2. 發(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ā)全流程

消息轉(zhuǎn)發(fā)機(jī)制處理消息的各個(gè)步驟.png

接收者在每一步中均有機(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è)方法分別以autoDictionarySetterautoDictionaryGetter函數(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):

  1. 若對(duì)象無(wú)法響應(yīng)某個(gè)選擇子,則進(jìn)入消息轉(zhuǎn)發(fā)流程。
  2. 通過(guò)運(yùn)行期的動(dòng)態(tài)方法解析功能,我們可以在需要用到某個(gè)方法時(shí)再將其加入類中。
  3. 對(duì)象可以把其無(wú)法解讀的某些選擇子轉(zhuǎn)交給其他對(duì)象來(lái)處理。
  4. 經(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之上。


NSString類的選擇子映射表.png

Objective-C運(yùn)行期系統(tǒng)提供的幾個(gè)方法都能夠用來(lái)操作這張表。開發(fā)者可以向其中新增選擇子,也可以改變某選擇子所對(duì)應(yīng)的方法實(shí)現(xiàn),還可以交換兩個(gè)選擇子所映射到的指針。 經(jīng)過(guò)幾次操作之后,類的方法表就會(huì)變成下圖這個(gè)樣子。


經(jīng)過(guò)數(shù)字操作之后的NSString選擇子映射表.png

在新的映射表中,多了一個(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方法互換,交換之后的方法表如圖:


交互lowercaseString與eoc_myLowercaseString的方法實(shí)現(xiàn).png

這段代碼看上去好像會(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):

  1. 在運(yùn)行期,可以向類中新增或替換選擇子所對(duì)應(yīng)的方法實(shí)現(xiàn)。
  2. 使用另一份實(shí)現(xiàn)來(lái)替換原有的方法實(shí)現(xiàn),這道工序叫做“方法調(diào)配”,開發(fā)者常用此 技術(shù)向原有實(shí)現(xiàn)中添加新功能。
  3. 一般來(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),則其繼承體系如圖


SomeClass實(shí)例所屬的“類繼承體系”,此類繼承自NSObject,圖中也畫出了兩個(gè)對(duì)應(yīng)“元類”之間的繼承關(guān)系.png

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):

  1. 每個(gè)實(shí)例都有一個(gè)指向Class對(duì)象的指針,用以表明其類型,而這些Class對(duì)象則構(gòu)成了類的繼承體系。
  2. 如果對(duì)象類型無(wú)法在編譯期確定,那么就應(yīng)該使用類型信息査詢方法來(lái)探知。
  3. 盡量使用類型信息査詢方法來(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è)字母的。

不僅是類名,還有以下:

  1. 那么一定要給“分類” (category)及“分類”中的方法加上前綴(第25條解釋了這么做的原因)。
  2. 類的實(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):

  1. 選擇與你的公司、應(yīng)用程序或二者皆有關(guān)聯(lián)之名稱作為類名的前綴,并在所有代碼中均使用這一前綴。
  2. 若自己所開發(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):

  1. 在類中提供一個(gè)全能初始化方法,并于文檔里指明。其他初始化方法均應(yīng)調(diào)用此方法。
  2. 若全能初始化方法與超類不同,則需覆寫超類中的對(duì)應(yīng)方法。
  3. 如果超類的初始化方法不適用于子類,那么應(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):

  1. 實(shí)現(xiàn)description方法返回一個(gè)有意義的字符串,用以描述該實(shí)例。
  2. 若想在調(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和類型查詢:

  1. 現(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。
  2. 有些“愛用蠻力的”(brutal)程序員甚至不通過(guò)“設(shè)置方法”,而是直接用類型信息查詢功能査出屬性所對(duì)應(yīng)的實(shí)例變量在內(nèi)存布局中的偏移量,以此來(lái)人為設(shè)置這個(gè)實(shí)例變量的值。這樣做比繞過(guò)本類的公共API還要不合規(guī)范

對(duì)于集合:

  1. 如果把可變對(duì)象(mutable object)放入collection之后又修改其內(nèi)容,那么很容易就會(huì)破壞set的內(nèi)部數(shù)據(jù)結(jié)構(gòu),使其失去固有的語(yǔ)義。因此,筆者建議大家盡量減少對(duì)象中的可變內(nèi)容。
  2. 對(duì)象里表示各種collection的那些屬性究競(jìng)應(yīng)該設(shè)成可變的,還是不可變的。在這種情況下,通常應(yīng)該提供一個(gè)readonly屬性供外界使用,該屬性將返回不可變的set, 而此set則是內(nèi)部那個(gè)可變set的一份拷貝。

要點(diǎn):

  1. 盡量創(chuàng)建不可變的對(duì)象。
  2. 若某屬性僅可于對(duì)象內(nèi)部修改,則在“class-continuation分類”中將其由readonly屬性擴(kuò)展為readwrite屬性,屬性的其他特質(zhì)必須保持不變。
  3. 不要把可變的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ī)則:

  1. 如果方法的返回值是新創(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)命名。
  2. 應(yīng)該把表示參數(shù)類型的名詞放在參數(shù)前面。
  3. 如果方法要在當(dāng)前對(duì)象上執(zhí)行操作,那么就應(yīng)該包含動(dòng)詞;若執(zhí)行操作時(shí)還需要參數(shù), 則應(yīng)該在動(dòng)詞后面加上一個(gè)或多個(gè)名詞。
  4. 不要使用str這種簡(jiǎn)稱,應(yīng)該用string這樣的全稱。
  5. Boolean屬性應(yīng)加is前綴。如果某方法返回非屬性的Boolean值,那么應(yīng)該根據(jù)其功能,選用has或is當(dāng)前綴。
  6. 將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):

  1. 起名時(shí)應(yīng)遵從標(biāo)準(zhǔn)的Objective-C命名規(guī)范,這樣創(chuàng)建出來(lái)的接口更容易為開發(fā)者所 理解。
  2. 方法名要言簡(jiǎn)意賅,從左至右讀起來(lái)要像個(gè)日常用語(yǔ)中的句子才好。
  3. 方法名里不要使用縮略后的類型名稱。
  4. 給方法起名時(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):

  1. 給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區(qū)分開。
  2. 不要單用一個(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):

  1. 只有發(fā)生了可使整個(gè)應(yīng)用程序崩潰的嚴(yán)重錯(cuò)誤時(shí),才應(yīng)使用異常。
  2. 在錯(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í)行深拷貝:


淺拷貝與深拷貝對(duì)比圖。淺拷貝之后的內(nèi)容與原始內(nèi)容均指向相同對(duì)象。 而深拷貝之后的內(nèi)容所指的對(duì)象是原始內(nèi)容中相關(guān)對(duì)象的一份拷貝.png
-(id)initWithSet:(NSArray^)array copyltems:(BOOL)copyltems

若copyltem參數(shù)設(shè)為YES,則該方法會(huì)向數(shù)組中的每個(gè)元素發(fā)送copy消息,用拷貝好的元素創(chuàng)建新的set,并將其返回給調(diào)用者。

要點(diǎn):

  1. 若想令自己所寫的對(duì)象具有拷貝功能,需實(shí)現(xiàn)NSCopying協(xié)議。
  2. 如果自定義的對(duì)象分為可變版本與不可變版本,那么就要同時(shí)實(shí)現(xiàn)NSCopying與 NSMutableCopying 協(xié)議。
  3. 復(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)此模式:

  1. 委托協(xié)議名通常是在相關(guān)類名后面加上Delegate—詞,整個(gè)類名采用“駝峰法”來(lái)寫。
  2. 有了這個(gè)協(xié)議之后,類就可以用一個(gè)屬性來(lái)存放其委托對(duì)象了。
  3. 實(shí)現(xiàn)委托對(duì)象的辦法是聲明某個(gè)類遵從委托協(xié)議(一般都是在“class-continuation分類”里聲明的)
  4. 把協(xié)議中想實(shí)現(xiàn)的那些方法在類里實(shí)現(xiàn)出來(lái)。

注意點(diǎn):

  1. 存放委托對(duì)象的屬性需定義成weak,而非strong,因?yàn)閮烧咧g必須為“非擁有關(guān)系” (nonowning relationship)。通常情況下,扮演delegate的那個(gè)對(duì)象也要持有本對(duì)象。
  2. 如果要在委托對(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ù)源流向類,而在普通的委托模式中,信息則從類流向受委托者


數(shù)據(jù)源.png

比方說(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):

  1. 委托模式為對(duì)象提供了一套接口,使其可由此將相關(guān)事件告知其他對(duì)象。
  2. 將委托對(duì)象應(yīng)該支持的接口定義成協(xié)議,在協(xié)議中把可能需要處理的事件定義成方法。
  3. 當(dāng)某對(duì)象需要從另外一個(gè)對(duì)象中獲取數(shù)據(jù)時(shí),可以使用委托模式。這種情境下,該模式亦稱“數(shù)據(jù)源協(xié)議”(data source protocal)。
  4. 若有必要,可實(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í)一并引入分類的頭文件。以下是原因:

  1. 隨著分類數(shù)量增加,當(dāng)前這份實(shí)現(xiàn)文件很快就膨脹得無(wú)法管理了。
  2. 便于調(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):

  1. 使用分類機(jī)制把類的實(shí)現(xiàn)代碼劃分成易于管理的小塊。
  2. 將應(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):

  1. 向第三方類中添加分類時(shí),總應(yīng)給其名稱加上你專用的前綴。
  2. 向第三方類中添加分類時(shí),總應(yīng)給其中的方法名加上你專用的前綴。

第26條:勿在分類中聲明屬性

屬性是封裝數(shù)據(jù)的方式(參見第6條)。盡管從技術(shù)上說(shuō),分類里也可以聲明屬性,但這種做法還是要盡量避免。原因在于,除了 “class-continuation分類”(參見第27條)之外,其他分類都無(wú)法向類中新增實(shí)例變量,因此,它們無(wú)法把實(shí)現(xiàn)屬性所需的實(shí)例變量合成出來(lái)。

有兩種辦法可以在分類里聲明屬性:

  1. 把存取方法聲明為@dynamic, 也就是說(shuō),這些方法等到運(yùn)行期再提供,編譯器目前是看不見的。如果決定使用消息轉(zhuǎn)發(fā)機(jī)制(參見第12條)在運(yùn)行期攔截方法調(diào)用,并提供其實(shí)現(xiàn),那么或許可以采用這種做法。
  2. 關(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):

  1. 把封裝數(shù)據(jù)所用的全部屬性都定義在主接口里。
  2. 在“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):

  1. 通過(guò)“class-continuation分類”向類中新增實(shí)例變量。
  2. 如果某屬性在主接口中聲明為“只讀”,而類的內(nèi)部又要用設(shè)置方法修改此屬性,那 么就在“class-continuation分類”中將其擴(kuò)展為“可讀寫”。
  3. 把私有方法的原型聲明在“class-continuation分類”里面。
  4. 若想使類所遵循的協(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):

  1. 協(xié)議可在某種程度上提供匿名類型。具體的對(duì)象類型可以淡化成遵從某協(xié)議的id類型,協(xié)議里規(guī)定了對(duì)象所應(yīng)實(shí)現(xiàn)的方法。
  2. 使用匿名對(duì)象來(lái)隱藏類型名稱(或類名)。
  3. 如果具體類型不重要,重要的是對(duì)象能夠響應(yīng)(定義在協(xié)議里的)特定方法,那么可使用匿名對(duì)象來(lái)表示。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,441評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,475評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,834評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,009評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,559評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,516評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,728評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,249評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,484評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容