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
- 第二章 對象弧哎、消息盒至、運(yùn)行期
- 第11條:理解objc_msgSend的作用
- 第12條:理解消息轉(zhuǎn)發(fā)機(jī)制
- 第13條:用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”
- 第14條:理解“類對象”的用意
- 第三章 接口與API設(shè)計(jì)
- 第15條:用前綴避免命名空間沖突
- 第16條:提供“全能初始化方法”
- 第17條:實(shí)現(xiàn)description方法
- 第18條:盡量使用不可變對象
- 第19條:使用清晰而協(xié)調(diào)的命名方式
- 第20條:為私有方法名加前綴
- 第21條:理解Objective-C錯(cuò)誤模型
- 第22條:理解NSCopying協(xié)議
- 第四章 協(xié)議與分類
- 第23條:通過委托與數(shù)據(jù)源協(xié)議進(jìn)行對象間通信
- 第24條:將類的實(shí)現(xiàn)代碼分散到便于管理的數(shù)個(gè)分類當(dāng)中
- 第25條:總是為第三方類的分類名稱加前綴
- 第26條:勿在分類中聲明屬性
- 第27條:使用"class - continuation"分類隱藏實(shí)現(xiàn)細(xì)節(jié)
- 第28條:通過協(xié)議提供匿名對象
第11條:理解objc_msgSend的作用
在對象上調(diào)用方法是Objective-C中經(jīng)常使用的功能。用Objective-C的術(shù)語來說,這叫做“傳遞消息”(pass a message)智什。
由于Objective-C是C的超集,所以最好先理解C語言的函數(shù)調(diào)用方式丁屎。C語言使用“靜態(tài)綁定”(static binding),也就是說荠锭,在編譯期就能決定運(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語句里都有函數(shù)調(diào)用指令。而在第二個(gè)例子中培愁,只有一個(gè)函數(shù)調(diào)用指令著摔,不過待調(diào)用的函數(shù)地址無法硬編碼在指令之中,而是要在運(yùn)行期讀取出來定续。
在Objective-C中谍咆,如果向某對象傳遞消息,那就會(huì)使用動(dòng)態(tài)綁定機(jī)制來決定需要調(diào)用的方法摹察。在底層时肿,所有方法都是普通的C語言函數(shù),然而對象收到消息之后港粱,究竟該調(diào)用哪個(gè)方法則完全于運(yùn)行期決定螃成,甚至可以在程序運(yùn)行時(shí)改變,這些特性使得Objective-C成為一門真正的動(dòng)態(tài)語言查坪。
給對象發(fā)送消息可以這樣來寫:
id returnValue = [someObject messageName:parameter];
在本例中寸宏,someObject 叫做“接收者”(receiver), messageName 叫做“選擇子”(selector),選擇子指的就是方法的名字偿曙, “選擇子”與“方法”這兩個(gè)詞經(jīng)常交替使用氮凝。選擇子與參數(shù)合起來稱為“消息”(message)。編譯器看到此消息后望忆,將其轉(zhuǎn)換為一條標(biāo)準(zhǔn)的C語言函數(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ù)正是通過這張表格來尋找應(yīng)該執(zhí)行的方法并跳至其實(shí)現(xiàn)的傅是。objc_msgSend函數(shù)會(huì)依據(jù)接收者與選擇子的類型來調(diào)用適當(dāng)?shù)姆椒ā榱送瓿纱瞬僮鳎?該方法需要在接收者所屬的類中搜尋其“方法列表”(list of methods),如果能找到與選擇子名稱相符的方法蕾羊,就跳至其實(shí)現(xiàn)代碼喧笔。若是找不到,那就沿著繼承體系繼續(xù)向上査找龟再,等找到合適的方法之后再跳轉(zhuǎn)书闸。如果最終還是找不到相符的方法,那就執(zhí)行“消息轉(zhuǎn)發(fā)” (message forwarding)操作利凑。
這么說來浆劲,想調(diào)用一個(gè)方法似乎需要很多步驟。所幸objc_mSgSend會(huì)將匹配結(jié)果緩存在“快速映射表"(fast map)里面截碴,每個(gè)類都有這樣一塊緩存梳侨,若是稍后還向該類發(fā)送與選擇子相同的消息哲虾,那么執(zhí)行起來就很快了谈秫。當(dāng)然這種“快速執(zhí)行路徑”(fastpath)還是不如 “靜態(tài)綁定的函數(shù)調(diào)用操作’(statically bound function call)那樣迅速硕淑,不過只要把選擇子緩存起來赃份,也就不會(huì)慢很多,實(shí)際上苍日,消息派發(fā)(message dispatch)并非應(yīng)用程序的瓶頸所在拦耐。假如真是個(gè)瓶頸的話狼纬,那你可以只編寫純C函數(shù),在調(diào)用時(shí)根據(jù)需要种吸,把ObjectWe-C對象的狀態(tài)傳進(jìn)去恩闻。
前面講的這部分內(nèi)容只描述了部分消息的調(diào)用過程,其他“邊界情況"(edgecase)园爷。則需要交由Objective-C運(yùn)行環(huán)境中的另一些函數(shù)來處理:
-
objc_msgSend_stret
:如果待發(fā)送的消息要返回結(jié)構(gòu)體扰楼,那么可交由此函數(shù)處理。只有當(dāng)CPU的寄存器能夠容納得下消息返回類型時(shí)美浦,這個(gè)函數(shù)才能處理此消息弦赖。若是返回值無法容納于CPU寄存器中(比如說返回的結(jié)構(gòu)體太大了),那么就由另一個(gè)函數(shù)執(zhí)行派發(fā)浦辨。此時(shí)蹬竖,那個(gè)函數(shù)會(huì)通過分配在棧上的某個(gè)變量來處理消息所返回的結(jié)構(gòu)體。 -
objc_mSgSend_fpret
:如果消息返回的是浮點(diǎn)數(shù)流酬,那么可交由此函數(shù)處理币厕。在某些架構(gòu)的CPU中調(diào)用函數(shù)時(shí),需要對“浮點(diǎn)數(shù)寄存器’(floating-point register)做特殊處理, 也就是說芽腾,通常所用的objC_msgSend在這種情況下并不合適旦装。這個(gè)函數(shù)是為了處理 x86等架構(gòu)CPU中某些令人稍覺驚訝的奇怪狀況。 -
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)”這一操作變得更簡單些。
如果某函數(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)化對objc_mSgSend非常關(guān)鍵伏穆,如果不這么做的話拘泞,那么每次調(diào)用Objective-C方法之前,都需要為調(diào)用objC_mSgSend函數(shù)準(zhǔn)備“棧幀”枕扫,大家在“棧蹤跡”(stack trace)中可以看到這種“棧幀”陪腌。此外,若是不優(yōu)化,還會(huì)過早地發(fā)生“棧溢出” (stack overflow)現(xiàn)象诗鸭。
這樣也就理解染簇,為何在調(diào)試的時(shí)候,椙堪叮“回溯”(backtrace)信息中總是出現(xiàn)objC_mSgSend锻弓。
要點(diǎn):
- 消息由接收者、選擇子及參數(shù)構(gòu)成蝌箍。給某對象潑送消息"(invoke a message:也是“調(diào)用”的意思青灼,此處為了與“call”相區(qū)隔,將其臨時(shí)譯為“發(fā)送”妓盲,也可理解為“激發(fā)”杂拨、 “觸發(fā))”也就相當(dāng)于在該對象上“調(diào)用方法”(call a method)。
- 發(fā)給某對象的全部消息都要由“動(dòng)態(tài)消息派發(fā)系統(tǒng)”(dynamic message dispatch system) 來處理悯衬,該系統(tǒng)會(huì)査出對應(yīng)的方法弹沽,并執(zhí)行其代碼。
第12條:理解消息轉(zhuǎn)發(fā)機(jī)制
上面講解了對象的消息傳遞機(jī)制筋粗,本節(jié)講解對象在收到無法解讀的消息之后會(huì)發(fā)生什么情況贷币。
若想令類能理解某條消息,我們必須以程序碼實(shí)現(xiàn)出對應(yīng)的方法才行亏狰。但是役纹,在編譯期向類發(fā)送了其無法解讀的消息并不會(huì)報(bào)錯(cuò),因?yàn)樵谶\(yùn)行期可以繼續(xù)向類中添加方法暇唾,所以編譯器在編譯時(shí)還無法確知類中到底會(huì)不會(huì)有某個(gè)方法實(shí)現(xiàn)促脉。當(dāng)對象接收到無法解讀的消息后,就會(huì)啟動(dòng)“消息轉(zhuǎn)發(fā)"(message forwarding)機(jī)制策州,程序員可經(jīng)由此過程告沂對象應(yīng)該如何處理未知消息瘸味。
你可能早就遇到過經(jīng)由消息轉(zhuǎn)發(fā)流程所處理的消息了,只是未加留意够挂。如果在控制臺中看到下面這種提示信息旁仿,那就說明你曾向某個(gè)對象發(fā)送過一條其無法解讀的消息,從而啟動(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办悟,而該接收者無法理解名為lowercaseString
的選擇子尘奏。本例所列舉的這種情況并不奇怪,因?yàn)镹SNumber類里本來就沒有名為lowercaseString的方法病蛉§偶樱控制臺中看到的那__NSFCNumber是為了實(shí)現(xiàn)“無縫橋接"(toll-free bridging瑰煎,后續(xù)將會(huì)詳解此技術(shù))而使用的內(nèi)部類(internal class),配置NSNumber對象時(shí)也會(huì)一并創(chuàng)建此對象。在本例中俗孝,消息轉(zhuǎn)發(fā)過程以應(yīng)用程序崩潰而告終酒甸,不過,開發(fā)者在編寫自己的類時(shí)赋铝,可于轉(zhuǎn)發(fā)過程中設(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í)行完了原环,那么接收者自己就無法再以動(dòng)態(tài)新增方法的手段來響應(yīng)包含該選擇子的消息了嘱吗。此時(shí)谒麦,運(yùn)行期系統(tǒng)會(huì)請求接收者以其他手段來處理與消息相關(guān)的方法調(diào)用弄匕。這又細(xì)分為兩小步迁匠。首先城丧,請接收者看看有沒有其他對象能處理這條消息亡哄。若有蚊惯,則運(yùn)行期系統(tǒng)會(huì)把消息轉(zhuǎn)給那個(gè)對象截型,于是消息轉(zhuǎn)發(fā)過程結(jié)束宦焦,一切如常。若沒有“備援的接收者”(replacement receiver),則啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制孵淘,運(yùn)行期系統(tǒng)會(huì)把與消息有關(guān)的全部細(xì)節(jié)都封裝到NSInvocation對象中瘫证,再給接收者最后一次機(jī)會(huì)滋捶,令其設(shè)法解決當(dāng)前還未處理的這條消息载萌。
動(dòng)態(tài)方法解析
對象在收到無法解讀的消息后扭仁,首先將調(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)插在類里面就可以了恕酸。此方案常用來實(shí)現(xiàn)@dynamic屬性蕊温,因?yàn)閷?shí)現(xiàn)這些屬性所需的存取方法在編譯期就能確定。
備援接收者
當(dāng)前接收者還有第二次機(jī)會(huì)能處理未知的選擇子凉翻,在這一步中制轰,運(yùn)行期系統(tǒng)會(huì)問它:能不能把這條消息轉(zhuǎn)給其他接收者來處理残炮。與該步驟對應(yīng)的處理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector
方法參數(shù)代表未知的選擇子,若當(dāng)前接收者能找到備援對象脉漏,則將其返回,若找不到司忱, 就返回nil坦仍。通過此方案繁扎,我們可以用“組合”(composition)來模擬出“多重繼承”(multiple inheritance)的某些特性。在一個(gè)對象內(nèi)部提澎,可能還有一系列其他對象盼忌,該對象可經(jīng)由此方法將能夠處理某選擇子的相關(guān)內(nèi)部對象返回,這樣的話服协,在外界看來偿荷,好像是該對象親自處理了這些消息似的忍饰。
請注意艾蓝,我們無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息赢织。若是想在發(fā)送給備援接收者之前先修改消息內(nèi)容,那就得通過完整的消息轉(zhuǎn)發(fā)機(jī)制來做了八毯。
完整的消息轉(zhuǎn)發(fā)
如果轉(zhuǎn)發(fā)算法已經(jīng)來到這一步的話,那么唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制 了尿孔。首先創(chuàng)建NSInvocation對象活合,把與尚未處理的那條消息有關(guān)的全部細(xì)節(jié)都封于其中。 此對象包含選擇子告嘲、目標(biāo)(target)及參數(shù)橄唬。在觸發(fā)NSInvocation對象時(shí),“消息派發(fā)系統(tǒng)” (message-dispatch system)將親自出馬僧界,把消息指派給目標(biāo)對象咬腕。
此步驟會(huì)調(diào)用下列方法來轉(zhuǎn)發(fā)消息:
- (void)forwardlnvocation:(NSInvocation *)invocation
這個(gè)方法可以實(shí)現(xiàn)得很簡單:只需改變調(diào)用目標(biāo)涨共,使消息在新目標(biāo)上得以調(diào)用即可。然而這樣實(shí)現(xiàn)出來的方法與“備援接收者”方案所實(shí)現(xià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)用請求挠进,直至NSObject。如果最后調(diào)用了 NSObject類的方法君旦,那么該方法還會(huì)繼而調(diào)用doesNotRecognizeSelector:
以拋出異常, 此異常表明選擇子最終未能得到處理恕稠。
消息轉(zhuǎn)發(fā)全流程
接收者在每一步中均有機(jī)會(huì)處理消息。步驟越往后,處理消息的代價(jià)就越大凑懂。最好能在第一步就處理完,這樣的話脓豪,運(yùn)行期系統(tǒng)就可以將此方法緩存起來了。如果這個(gè)類的實(shí)例稍后還收到同名選擇子笤闯,那么根本無須啟動(dòng)消息轉(zhuǎn)發(fā)流程超陆。
以完整的例子演示動(dòng)態(tài)方法解析
為了說明消息轉(zhuǎn)發(fā)機(jī)制的意義,下面示范如何以動(dòng)態(tà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í)例上訪問某個(gè)屬性時(shí)嘉裤,運(yùn)行期系統(tǒng)還找不到對應(yīng)的選擇子屑宠,因?yàn)樗璧倪x擇子既沒有直接實(shí)現(xiàn)躺翻,也沒有合成出來∮淮荆現(xiàn)在假設(shè)要寫入opaqueObject
屬性,那么系統(tǒng)就會(huì)以setOpaqueObject:
為選擇子來調(diào)用上面這個(gè)方法雹舀。 同理,在讀取該屬性時(shí)签财,系統(tǒng)也會(huì)調(diào)用上述方法灸叼,只不過傳入的選擇子是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的用法很簡單:
EOCAutoDictionary *dict = [EOCAutoDictionary new];
dict.date = [NSDate dateWithTimeIntervalSincel970:475372800];
NSLog(@"dict.date = %@",dict.date);
// Output: diet.date = 1985-01-24 00:00:00 +0000
其他屬性的訪問方式與date類似殊霞,要想添加新屬性摧阅,只需來定義,并將其聲明為@dynamic即可绷蹲。在iOS的CoreAnimation框架中棒卷,CALayer類就用了與本例相似的實(shí)現(xiàn)方式,這使得CALayer成為兼容于“鍵值編碼的”(key-value-coding-compliant:除了使用存取方法和“點(diǎn)語法”之外比规,還可以用字符串做鍵若厚,通過valueForKey:
與setValue:forKey:
這種形式來訪問屬性) 容器類, 也就等于說蜒什,能夠向里面隨意添加屬性测秸,然后以鍵值對的形式來訪問。于是灾常,開發(fā)者就可以向其中新增自定義的屬性了霎冯,這些屬性值的存儲工作由基類直接負(fù)責(zé),我們只需在CALayer 的子類中定義新屬性即可钞瀑。
要點(diǎn):
- 若對象無法響應(yīng)某個(gè)選擇子沈撞,則進(jìn)入消息轉(zhuǎn)發(fā)流程。
- 通過運(yùn)行期的動(dòng)態(tài)方法解析功能仔戈,我們可以在需要用到某個(gè)方法時(shí)再將其加入類中关串。
- 對象可以把其無法解讀的某些選擇子轉(zhuǎn)交給其他對象來處理。
- 經(jīng)過上述兩步之后监徘,如果還是沒辦法處理選擇子晋修,那就啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制。
第13條:用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”
上面講了:Objective-C對象收到消息之后凰盔,方法需要在運(yùn)行期才能解析出來墓卦。那么與給定的選擇子名稱相對應(yīng)的方法也可以在運(yùn)行期改變。若能善用此特性落剪,則可發(fā)揮出巨大優(yōu)勢抄瑟,因?yàn)槲覀兗炔恍枰创a鞋拟,也不需要通過繼承子類來覆寫方法就能改變這個(gè)類本身的功能。這樣一來猴誊,新功能將在本類的所有實(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ù)指針的形式來表示掺喻,這種指針叫做IMP, 其原型如下:
id (*IMP) (id, SEL,…)
NSString 類可以響應(yīng)lowercaseString、uppercaseString、capitalizedString等選擇子谦絮。這張映射表中的每個(gè)選擇子都映射到了不同的IMP之上赠潦。
Objective-C運(yùn)行期系統(tǒng)提供的幾個(gè)方法都能夠用來操作這張表。開發(fā)者可以向其中新增選擇子绷跑,也可以改變某選擇子所對應(yīng)的方法實(shí)現(xiàn),還可以交換兩個(gè)選擇子所映射到的指針垦藏。 經(jīng)過幾次操作之后,類的方法表就會(huì)變成下圖這個(gè)樣子。
在新的映射表中,多了一個(gè)名為newSelector的選擇子辛馆,capitalizedString的實(shí)現(xiàn)也變了诱咏, 而lowercaseString與uppercaseString的實(shí)現(xiàn)則互換了。上述修改均無須編寫子類同蜻,只要修改了“方法表”的布局砌梆,就會(huì)反映到程序中所有的NSString實(shí)例之上杖虾。
在實(shí)際應(yīng)用中,直接交換兩個(gè)方法實(shí)現(xiàn)的意義并不大。我們一般使用該手段來為既有的方法實(shí)現(xiàn)增添新功能。 比方說茄茁,想要在調(diào)用lowercaseString時(shí)記錄某些信息,這時(shí)就可以通過交換方法實(shí)現(xiàn)來達(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)則可通過下列函數(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)滋恬,不過大家要記住带斑,此方法是準(zhǔn)備和lowercaseString方法互換的。所以陷谱,在運(yùn)行期,eoc_myLowercaseString選擇子實(shí)際上對應(yīng)于原有的lowercaseString方法實(shí)現(xiàn)驳庭。
通過此方案,開發(fā)者可以為那些“完全不知道其具體實(shí)現(xiàn)的"(completely opaque, “完全不透明的”)黑盒方法增加日志記錄功能霎俩,這非常有助于程序調(diào)試。然而,此做法只在調(diào)試程序時(shí)有用。很少有人在調(diào)試程序之外的場合用上述“方法調(diào)配技術(shù)”來永久改動(dòng)某個(gè)類的功能藻糖。不能僅僅因?yàn)镺bjective-C語言里有這個(gè)特性就一定要用它。若是濫用虑乖,反而會(huì)令代碼變得不易讀懂且難于維護(hù)签钩。
要點(diǎn):
- 在運(yùn)行期,可以向類中新增或替換選擇子所對應(yīng)的方法實(shí)現(xiàn)柠并。
- 使用另一份實(shí)現(xiàn)來替換原有的方法實(shí)現(xiàn)锋爪,這道工序叫做“方法調(diào)配”否副,開發(fā)者常用此 技術(shù)向原有實(shí)現(xiàn)中添加新功能。
- 一般來說崎坊,只有調(diào)試程序的時(shí)候才需要在運(yùn)行期修改方法實(shí)現(xiàn)备禀,這種做法不宜濫用。
第14條:理解“類對象”的用意
Objective-C實(shí)際上是一門極其動(dòng)態(tài)的語言奈揍。第11條講解了運(yùn)行期系統(tǒng)如何査找并調(diào)用某方法的實(shí)現(xiàn)代碼曲尸,第12條則講述了消息轉(zhuǎn)發(fā)的原理:如果類無法立即響應(yīng)某個(gè)選擇子, 那么就會(huì)啟動(dòng)消息轉(zhuǎn)發(fā)流程。然而男翰,消息的接收者究竟是何物另患?是對象本身嗎?運(yùn)行期系統(tǒng)如何知道某個(gè)對象的類型呢蛾绎?對象類型并非在編譯期就綁定好了昆箕,而是要在運(yùn)行期査找鸦列。而且,還有個(gè)特殊的類型叫做id鹏倘,它能指代任意的Objective-C對象類型薯嗤。一般情況下,應(yīng)該指明消息接收者的具體類型纤泵,這樣的話骆姐,如果向其發(fā)送了無法解讀的消息,那么編譯器就會(huì)產(chǎn)生警告信息捏题。而類型為id的對象則不然玻褪,編譯器假定它能響應(yīng)所有消息。
如果看過第12條公荧,你就會(huì)明白归园,編譯器無確定某類型對象到底能解讀多少種選擇子, 因?yàn)檫\(yùn)行期還可向其中動(dòng)態(tài)新增。然而稚矿,即便使用了動(dòng)態(tài)新增技術(shù),編譯器也覺得應(yīng)該能在某個(gè)頭文件中找到方法原型的定義捻浦,據(jù)此可了解完整的“方法簽名"(method signature)晤揣,并生成派發(fā)消息所需的正確代碼。
“在運(yùn)行期檢視對象類型”這一操作也叫做“類型信息査詢”(introspection, “內(nèi)省”)朱灿,這個(gè)強(qiáng)大而有用的特性內(nèi)置于Foundation框架的NSObject協(xié)議里昧识,凡是由公共根類(common root class,即NSObject與NSProxy)繼承而來的對象都要遵從此協(xié)議盗扒。在程序中不要直接比較對象所屬的類跪楞,明智的做法是調(diào)用“類型信息査詢方法”,其原因筆者稍后解釋侣灶。不過在介紹類型信息査詢技術(shù)之前甸祭,我們先講一些基礎(chǔ)知識,看看Objective-C對象的本質(zhì)是什么褥影。
每個(gè)Objective-C對象實(shí)例都是指向某塊內(nèi)存數(shù)據(jù)的指針(池户??凡怎?)校焦。所以在聲明變量時(shí),類型后面要跟一個(gè)字符:
NSString *pointerVariable = @"Some string";
編過C語言程序的人都知道這是什么意思统倒。對于沒寫過C語言的程序員來說, pointerVariable可以理解成存放內(nèi)存地址的變量寨典,而NSString自身的數(shù)據(jù)就存于那個(gè)地址中。 因此可以說房匆,該變量“指向”(point to) NSString實(shí)例耸成。所有Objective-C對象都是如此报亩,若是想把對象所需的內(nèi)存分配在棧上,編譯器則會(huì)報(bào)錯(cuò):
String stackVariable = @"Some string";
"error: interface type cannot be statically allocated
對于通用的對象類型id,由于其本身已經(jīng)是指針了墓猎,所以我們能夠這樣寫:
id genericTypedString = @"Some string";
上面這種定義方式與用NSString *
來定義相比捆昏,其語法意義相同。唯一區(qū)別在于毙沾,如果聲明時(shí)指定了具體類型骗卜,那么在該類實(shí)例上調(diào)用其所沒有的方法時(shí),編譯器會(huì)探知此情況左胞,并發(fā)出警告信息寇仓。
描述Objective-C對象所用的數(shù)據(jù)結(jié)構(gòu)定義在運(yùn)行期程序庫的頭文件里,id類型本身也在定義在這里:
typedef struct objc_object {
Class isa;
} *id;
由此可見烤宙,每個(gè)對象結(jié)構(gòu)體的首個(gè)成員是Class類的變量遍烦。該變量定義了對象所屬的類, 通常稱為isa
指針。例如躺枕,剛才的例子中所用的對象“是一個(gè)”(isa) NSString服猪,所以其“isa”指針就指向NSString。
Class對象也定義在運(yùn)行期程序庫的頭文件中:
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指針叉瘩,這說明Class本身亦為Objective-C對象膳帕。 結(jié)構(gòu)體里還有個(gè)變量叫做superclass,它定義了本類的超類。類對象所屬的類型(也就是isa 指針?biāo)赶虻念愋停┦橇硗庖粋€(gè)類薇缅,叫做“元類"(metadass)危彩,用來表述類對象本身所具備的元數(shù)據(jù)∮捐耄“類方法”就定義于此處汤徽,因?yàn)檫@些方法可以理解成類對象的實(shí)例方法。每個(gè)類僅有一個(gè)“類對象”灸撰,而每個(gè)“類對象”僅有一個(gè)與之相關(guān)的“元類”泻骤。
假設(shè)有個(gè)名為SomeClass的子類從NSObject中繼承而來,則其繼承體系如圖
superclass指針確立了繼承關(guān)系,而isa指針描述了實(shí)例所屬的類亲轨。通過這張布局關(guān)圖即可執(zhí)行“類型信息査詢”趋惨。我們可以査出對象是否能響應(yīng)某個(gè)選擇子,是否遵從某項(xiàng)協(xié)議惦蚊,并且能看出此對象位于“類繼承體系”(class hierarchy)的哪一部分器虾。
在類繼承體系中查詢類型信息
可以用類型信息査詢方法來檢視類繼承體系讯嫂。isMemberOfClass:
能夠判斷出對象是否為某個(gè)特定類的實(shí)例,而isKindOfClass:
則能夠判斷出對象是否為某類或其派生類的實(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指針獲取對象所屬的類欧芽,然后通過superclass指針在繼承體系中游走。由于對象是動(dòng)態(tài)的葛圃,所以此特性顯得極為重要千扔。Objective-C與你可能熟悉的其他語言不同,在此語言中库正,必須査詢類型信息曲楚,方能完全了解對象的真實(shí)類型。
由于Objective-C使用“動(dòng)態(tài)類型系統(tǒng)"(dynamic typing)褥符,所以用于査詢對象所屬類的類型信息査詢功能非常有用龙誊。從collection中獲取對象時(shí),通常會(huì)査詢類型信息喷楣,這些對象不是“強(qiáng)類型的”(strongly typed)趟大,把它們從collection中取出來時(shí),其類型通常是id铣焊。如果想知道具體類型护昧,那就可以使用類型信息査詢方法。
也可以用比較類對象是否等同的辦法來做粗截。若是如此,那就要使用==操作符捣炬,而不要使用比較Objective-C對象時(shí)常用的“isEqual:”方法(參見第8條)熊昌。原因在于,類對象是 “單例”(singleton),在應(yīng)用程序范圍內(nèi)湿酸,每個(gè)類的Class僅有一個(gè)實(shí)例婿屹。也就是說,另外一種可以精確判斷出對象是否為某類實(shí)例的辦法是:
id object = /* ??? */;
if ([object class] == [EOCSomeClass class]) {
//'object' is an instance of EOCSomeClass
)
即便能這樣做推溃,我們也應(yīng)該盡景使用類型信息査詢方法昂利,而不應(yīng)該直接比較兩個(gè)類對象是否等同,因?yàn)榍罢呖梢哉_處理那些使用了消息傳遞機(jī)制(參見第12條)的對象铁坎。比方說蜂奸,某個(gè)對象可能會(huì)把其收到的所有選擇子都轉(zhuǎn)發(fā)給另外一個(gè)對象。這樣的對象叫做“代理” (proxy)硬萍,此種對象均以NSProxy為根類扩所。
通常情況下,如果在此種代理對象上調(diào)用class方法朴乖,那么返回的是代理對象本身(此類是NSProxy的子類)祖屏,而非接受的代理的對象所屬的類助赞。然而,若是改用isKindOfClass:
這樣的類型信息査詢方法袁勺,那么代理對象就會(huì)把這條消息轉(zhuǎn)給“接受代理的對象”(proxied object)雹食。也就是說,這條消息的返回值與直接在接受代理的對象上面査詢其類型所得的結(jié)果相同期丰。因此群叶,這樣?xùn)顺鰜淼念悓ο笈c通過class方法所返回的那個(gè)類對象不同,class方法所返回的類表示發(fā)起代理的對象咐汞,而非接受代理的對象盖呼。
要點(diǎn):
- 每個(gè)實(shí)例都有一個(gè)指向Class對象的指針,用以表明其類型化撕,而這些Class對象則構(gòu)成了類的繼承體系几晤。
- 如果對象類型無法在編譯期確定,那么就應(yīng)該使用類型信息査詢方法來探知植阴。
- 盡量使用類型信息査詢方法來確定對象類型蟹瘾,而不要直接比較類對象,因?yàn)槟承ο罂赡軐?shí)現(xiàn)了消息轉(zhuǎn)發(fā)功能掠手。
第三章 接口與API設(shè)計(jì)
第15條:用前綴避免命名空間沖突
Objective-C沒有其他語言那種內(nèi)置的命名空間(namespace)機(jī)制憾朴。鑒于此,我們在起名時(shí)要設(shè)法避免潛在的命名沖突喷鸽,否則很容易就重名了众雷。如果發(fā)生命名沖突(naming dash), 那么應(yīng)用程序的鏈接過程就會(huì)出錯(cuò)做祝,因?yàn)槠渲谐霈F(xiàn)了重復(fù)符號:
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
避免此問題的唯一辦法就是變相實(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è)問題必須要注意悯嗓。因?yàn)樵诰幾g好的目標(biāo)文件中件舵,這些名稱是要算作“頂級符號”(top-level symbol)的。
這么做的好處:若此符號出現(xiàn)在椄回溯信息中芦圾,則很容易就能判明問題源自哪塊代碼。
要點(diǎn):
- 選擇與你的公司俄认、應(yīng)用程序或二者皆有關(guān)聯(lián)之名稱作為類名的前綴个少,并在所有代碼中均使用這一前綴洪乍。
- 若自己所開發(fā)的程序庫中用到了第三方庫,則應(yīng)為其中的名稱加上前綴夜焦。
第16條: 提供“全能初始化方法”
為對象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(designated initializer) 壳澳。令其他初始化方法都來調(diào)用它。只有在全能初始化方法中茫经,才會(huì)存儲內(nèi)部數(shù)據(jù)巷波。這樣的話,當(dāng)?shù)讓訑?shù)據(jù)存儲機(jī)制改變時(shí)卸伞,只需修改此方法的代碼就好抹镊,無須改動(dòng)其他初始化方法。
每個(gè)子類的全能初始化方法都應(yīng)該調(diào)用其超類的對應(yīng)方法荤傲,并逐層向上垮耳。
要點(diǎn):
- 在類中提供一個(gè)全能初始化方法,并于文檔里指明遂黍。其他初始化方法均應(yīng)調(diào)用此方法终佛。
- 若全能初始化方法與超類不同,則需覆寫超類中的對應(yīng)方法雾家。
- 如果超類的初始化方法不適用于子類铃彰,那么應(yīng)該覆寫這個(gè)超類方法,并在其中拋出異常芯咧。
第17條:實(shí)現(xiàn)description方法
調(diào)試程序時(shí)牙捉,經(jīng)常需要打印并查看對象信息。構(gòu)建需要打印到日志的字符串時(shí)敬飒,object對象會(huì)收到description消息邪铲,該方法所返回的描述信息將取代“格式字符串”(format string:在這里指%@)。
當(dāng)自定義對象驶拱,并想輸出更為有用的信息也很簡單,只需覆寫description方法并將描述此對象的字符串返回即可晶衷。
NSObject協(xié)議中還有個(gè)方法要注意蓝纲,那就是debugDescription,此方法的用意與description非常相似晌纫。二者區(qū)別在于税迷,debugDescription方法是開發(fā)者在調(diào)試器(debugger) 中以控制臺命令(比如LLDB的po命令
)打印對象時(shí)才調(diào)用的。在NSObject類的默認(rèn)實(shí)現(xiàn)中锹漱,此方法只是直接調(diào)用了description 箭养。
要點(diǎn):
- 實(shí)現(xiàn)description方法返回一個(gè)有意義的字符串,用以描述該實(shí)例哥牍。
- 若想在調(diào)試時(shí)打印出更詳盡的對象描述信息毕泌,則應(yīng)實(shí)現(xiàn)debugDescription方法喝检。
第18條:盡量使用不可變對象
設(shè)計(jì)類的時(shí)候,應(yīng)充分運(yùn)用屬性來封裝數(shù)據(jù)(參見第6條)撼泛。默認(rèn)情況下挠说,屬性是“既可讀又可寫的"(read-write),這樣設(shè)計(jì)出來的類都是“可變的”(mutable)。具體到編程實(shí)踐中愿题,則應(yīng)該盡量把對外公布出來的屬性設(shè)為只讀损俭,而且只在確有必要時(shí)才將屬性對外公布。
當(dāng)然潘酗,如果該屬性是nonatomic的杆兵,那么這樣做可能會(huì)產(chǎn)生“競爭條件”(racecondition)。在對象內(nèi)部寫入某屬性時(shí)仔夺,對象外的觀察者也許正讀取該屬性。若想避免此問題囚灼,我們可以在必要時(shí)通過“派發(fā)隊(duì)列"(dispatch queue, 參見第41條)等手段政钟,將(包括對象內(nèi)部的)所有數(shù)據(jù)存取操作都設(shè)為同步操作养交。
kvc和類型查詢:
- 現(xiàn)在,只能于實(shí)現(xiàn)代碼內(nèi)部設(shè)置這些屬性值了鱼辙。其實(shí)更準(zhǔn)確地說, 在對象外部玫镐,仍然能通過“鍵值編碼”(Key-Value Coding倒戏,KVC)技術(shù)設(shè)置這些屬性值,比如說:
[pointOfInterest setValue:@"abc forKey:@"identifier"];
這樣做可以改動(dòng)identifier屬性恐似,因?yàn)镵VC會(huì)在類里査找setIdentifier:
方法杜跷,并借此修改此屬性。不過,這樣做等于違規(guī)地繞過了本類所提供的API葛闷。 - 有些“愛用蠻力的”(brutal)程序員甚至不通過“設(shè)置方法”憋槐,而是直接用類型信息查詢功能査出屬性所對應(yīng)的實(shí)例變量在內(nèi)存布局中的偏移量,以此來人為設(shè)置這個(gè)實(shí)例變量的值孵运。這樣做比繞過本類的公共API還要不合規(guī)范
對于集合:
- 如果把可變對象(mutable object)放入collection之后又修改其內(nèi)容秦陋,那么很容易就會(huì)破壞set的內(nèi)部數(shù)據(jù)結(jié)構(gòu),使其失去固有的語義治笨。因此驳概,筆者建議大家盡量減少對象中的可變內(nèi)容。
- 對象里表示各種collection的那些屬性究競應(yīng)該設(shè)成可變的旷赖,還是不可變的顺又。在這種情況下,通常應(yīng)該提供一個(gè)readonly屬性供外界使用等孵,該屬性將返回不可變的set, 而此set則是內(nèi)部那個(gè)可變set的一份拷貝稚照。
要點(diǎn):
- 盡量創(chuàng)建不可變的對象。
- 若某屬性僅可于對象內(nèi)部修改俯萌,則在“class-continuation分類”中將其由readonly屬性擴(kuò)展為readwrite屬性果录,屬性的其他特質(zhì)必須保持不變。
- 不要把可變的collection作為屬性公開咐熙,而應(yīng)提供相關(guān)方法弱恒,以此修改對象中的可變collection。
第19條:使用清晰而協(xié)調(diào)的命名方式
方法與變量名使用了“駝峰式大小寫命名法"(camel casing)-以小寫字母開頭棋恼,其后每個(gè)單詞首字母大寫返弹。類名也用駝峰命名法,不過其首字母要大寫爪飘,而且前面通常還有兩三個(gè)前綴字母(參見第15條)义起。在編寫Objective-C代碼時(shí),大家一般都使用這種命名方式师崎。
給方法命名時(shí)的注意事項(xiàng)可總結(jié)成下面幾條規(guī)則:
- 如果方法的返回值是新創(chuàng)建的默终,那么方法名的首個(gè)詞應(yīng)是返回值的類型,除非前面還有修飾語犁罩,例如localizedString齐蔽。屬性的存取方法不遵循這種命名方式,因?yàn)橐话阏J(rèn)為這些方法不會(huì)創(chuàng)建新對象昼汗,即便有時(shí)返回內(nèi)部對象的一份拷貝肴熏,我們也認(rèn)為那相當(dāng)于原有的對象鬼雀。這些存取方法應(yīng)該按照其所對應(yīng)的屬性來命名顷窒。
- 應(yīng)該把表示參數(shù)類型的名詞放在參數(shù)前面。
- 如果方法要在當(dāng)前對象上執(zhí)行操作,那么就應(yīng)該包含動(dòng)詞鞋吉;若執(zhí)行操作時(shí)還需要參數(shù), 則應(yīng)該在動(dòng)詞后面加上一個(gè)或多個(gè)名詞鸦做。
- 不要使用str這種簡稱,應(yīng)該用string這樣的全稱谓着。
- Boolean屬性應(yīng)加is前綴泼诱。如果某方法返回非屬性的Boolean值,那么應(yīng)該根據(jù)其功能赊锚,選用has或is當(dāng)前綴治筒。
- 將get這個(gè)前綴留給那些借由“輸出參數(shù)”來保存返回值的方法,比如說舷蒲,把返回值填充到“C語言式數(shù)組”(C-style array)里的那種方法就可以使用這個(gè)詞做前綴耸袜。
類與協(xié)議的命名
應(yīng)該為類與協(xié)議的名稱加上前綴,以避免命名空間沖突(參見第15條)牲平,如果要從其他框架中繼承子類堤框,那么務(wù)必遵循其命名慣例。比方說纵柿,要從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)建出來的接口更容易為開發(fā)者所 理解格带。
- 方法名要言簡意賅,從左至右讀起來要像個(gè)日常用語中的句子才好刹枉。
- 方法名里不要使用縮略后的類型名稱叽唱。
- 給方法起名時(shí)的第一要?jiǎng)?wù)就是確保其風(fēng)格與你自己的代碼或所要集成的框架相符。
第20條:為私有方法名加前綴
具體使用何種前綴可根據(jù)個(gè)人喜好來定微宝,其中最好包含字母P與下劃線棺亭。比如
- (void)p_add {
//...
}
如果寫過C++或Java代碼,你可能就會(huì)問了:為什么要這樣做呢蟋软?直接把方法聲明成私有的不就好了嗎镶摘? Objective-C語言沒辦法將方法標(biāo)為私有。每個(gè)對象都可以響應(yīng)任意消息(參見第12條)岳守,而且可在運(yùn)行期檢視某個(gè)對象所能直接響應(yīng)的消息(參見第14條)凄敢。根據(jù)給定的消息査出其對應(yīng)的方法,這一工作要在運(yùn)行期才能完成(參見第11條)湿痢,所以 Objective-C中沒有那種約束方法調(diào)用的機(jī)制用以限定誰能調(diào)用此方法涝缝、能在哪個(gè)對象上調(diào)用此方法以及何時(shí)能調(diào)用此方法扑庞。開發(fā)者會(huì)在命名慣例中體現(xiàn)出“私有方法”等語義。必須用心領(lǐng)悟Objective-C語言這種強(qiáng)大的動(dòng)態(tài)特性拒逮。
要點(diǎn):
- 給私有方法的名稱加上前綴罐氨,這樣可以很容易地將其同公共方法區(qū)分開。
- 不要單用一個(gè)下劃線做私有方法的前綴滩援,因?yàn)檫@種做法是預(yù)留給蘋果公司用的
第21條:理解Objective-C錯(cuò)誤模型
當(dāng)前很多種編程語言都有“異痴ひ”(exception)機(jī)制,Objective-C也不例外玩徊。寫過Java 代碼的程序員應(yīng)該很習(xí)慣于用異常來處理錯(cuò)誤租悄。如果你也是這么使用異常的,那現(xiàn)在就把它忘了吧恩袱,我們得從頭學(xué)起恰矩。
首先要注意的是,“自動(dòng)引用計(jì)數(shù)”(Automatic ReferenceCounting, ARC,參見第30條)在默認(rèn)情況下不是“異常安全的"(exception safe)憎蛤。具體來說外傅,這意味著:如果拋出異常,那么本應(yīng)在作用域末尾釋放的對象現(xiàn)在卻不會(huì)自動(dòng)釋放了俩檬。如果想生成“異常安全”的代碼萎胰, 可以通過設(shè)置編譯器的標(biāo)志來實(shí)現(xiàn),不過這將引入一些額外代碼棚辽,在不拋出異常時(shí)技竟,也照樣要執(zhí)行這部分代碼。需要打開的編譯器標(biāo)志叫做-fobjc-arc-exceptions
屈藐。
既然異常只用于處理嚴(yán)重錯(cuò)誤(fatal error,致命錯(cuò)誤)榔组,那么對其他錯(cuò)誤怎么辦呢?在出現(xiàn)“不那么嚴(yán)重的錯(cuò)誤"(nonfatalerror,非致命錯(cuò)誤)時(shí)联逻,Objective-C語言所用的編程范式為: 令方法返回nil/0,或是使用NSError搓扯,以表明其中有錯(cuò)誤發(fā)生。
要點(diǎn):
- 只有發(fā)生了可使整個(gè)應(yīng)用程序崩潰的嚴(yán)重錯(cuò)誤時(shí)包归,才應(yīng)使用異常锨推。
- 在錯(cuò)誤不那么嚴(yán)重的情況下,可以指派“委托方法”(delegate method)來處理錯(cuò)誤公壤,也可以把錯(cuò)誤信息放在NSError對象里换可,經(jīng)由“輸出參數(shù)”返回給調(diào)用者。
第22條:理解NSCopying協(xié)議
使用對象時(shí)經(jīng)常需要拷貝它厦幅。在Objective-C中沾鳄,此操作通過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), 而對象會(huì)創(chuàng)建在某個(gè)區(qū)里面〈沤罚現(xiàn)在不用了,每個(gè)程序只有一個(gè)區(qū):“默認(rèn)區(qū)”(default zone)玫芦。 所以說浆熔,盡管必須實(shí)現(xiàn)這個(gè)方法,但是你不必?fù)?dān)心其中的zone參數(shù)桥帆。
copy->_friends = [_friends mutableCopy];
這次所實(shí)現(xiàn)的方法比原來多了一些代碼医增,它把本對象的_friends實(shí)例變量復(fù)制了一份, 令copy對象的_friends實(shí)例變量指向這個(gè)復(fù)制過的set。(注意:這里使用了->語法老虫,因?yàn)?/em> friends并非屬性叶骨,只是個(gè)在內(nèi)部使用的實(shí)例變量。其實(shí)也可以聲明一個(gè)屬性來表示它祈匙,不過由于該變量不會(huì)在本類之外使用忽刽,所以那么做沒必要)
NSMutableCopying的協(xié)議。該協(xié)議與 NSCopying類似夺欲,也只定義了一個(gè)方法跪帝,然而方法名不同:
- (id)mutableCopyWithZone:(NSZone*)zone
在編寫拷貝方法時(shí),還要決定一個(gè)問題些阅,就是應(yīng)該執(zhí)行“深拷貝”(deep copy)還是“淺拷貝”(shallowcopy)伞剑。深拷貝的意思就是:在拷貝對象自身時(shí),將其底層數(shù)據(jù)也一并復(fù)制過去市埋。Foundation框架中的所有collection類在默認(rèn)情況下都執(zhí)行淺拷貝黎泣,也就是說,只拷貝容器對象本身缤谎,而不復(fù)制其中數(shù)據(jù)抒倚。這樣做的主要原因在于,容器內(nèi)的對象未必都能拷貝坷澡,而且調(diào)用者也未必想在拷貝容器時(shí)一并拷貝其中的每個(gè)對象衡便。
以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):
- 若想令自己所寫的對象具有拷貝功能,需實(shí)現(xiàn)NSCopying協(xié)議汁展。
- 如果自定義的對象分為可變版本與不可變版本鹊碍,那么就要同時(shí)實(shí)現(xiàn)NSCopying與 NSMutableCopying 協(xié)議厌殉。
- 復(fù)制對象時(shí)需決定采用淺拷貝還是深拷貝,一般情況下應(yīng)該盡量執(zhí)行淺拷貝侈咕。
第四章 協(xié)議與分類
Objective-C語言有一項(xiàng)特性叫做“協(xié)議”(protocol)公罕,它與Java的“接口"(interface)類似,Objective-C不支持多重繼承耀销,因而我們把某個(gè)類應(yīng)該實(shí)現(xiàn)的一系列方法定義在協(xié)議里面楼眷。 協(xié)議最為常見的用途是實(shí)現(xiàn)委托模式(參見第23條)软免,不過也有其他用法逛揩。
“分類”(Category)也是Objective-C的一項(xiàng)重要語言特性。利用分類機(jī)制满力,我們無須繼承子類即可直接為當(dāng)前類添加方法狰住,而在其他編程語言中张吉,則需通過繼承子類來實(shí)現(xiàn)。由于Objective-C運(yùn)行期系統(tǒng)是髙度動(dòng)態(tài)的催植,所以才能支持這一特性肮蛹,然而,其中也隱藏著一些陷阱创南。
第23條:通過委托與數(shù)據(jù)源協(xié)議進(jìn)行對象間通信
對象之間經(jīng)常需要相互通信蔗崎,而通信方式有很多種。Objective-C開發(fā)者廣泛使用一種名叫“委托模式"(Delegatepattem)的編程設(shè)計(jì)模式來實(shí)現(xiàn)對象間的通信扰藕,該模式的主旨是: 定義一套接口缓苛,某對象若想接受另一個(gè)對象的委托,則需遵從此接口邓深,以便成為其“委托對象”(delegate)未桥。而這“另一個(gè)對象”則可以給其委托對象回傳一些信息。
在Objective-C中芥备,一般通過“協(xié)議”這項(xiàng)語言特性來實(shí)現(xiàn)此模式:
- 委托協(xié)議名通常是在相關(guān)類名后面加上
Delegate
—詞冬耿,整個(gè)類名采用“駝峰法”來寫。 - 有了這個(gè)協(xié)議之后萌壳,類就可以用一個(gè)屬性來存放其委托對象了亦镶。
- 實(shí)現(xiàn)委托對象的辦法是聲明某個(gè)類遵從委托協(xié)議(一般都是在“class-continuation分類”里聲明的)
- 把協(xié)議中想實(shí)現(xiàn)的那些方法在類里實(shí)現(xiàn)出來。
注意點(diǎn):
- 存放委托對象的屬性需定義成
weak
,而非strong
袱瓮,因?yàn)閮烧咧g必須為“非擁有關(guān)系” (nonowning relationship)缤骨。通常情況下,扮演delegate的那個(gè)對象也要持有本對象尺借。 - 如果要在委托對象上調(diào)用可選方法绊起,那么必須提前使用類型信息査詢方法
respondsToSelector:
判斷這個(gè)委托對象能否響應(yīng)相關(guān)選擇子。
大家應(yīng)該很容易理解此模式為何叫做“委托模式”:因?yàn)閷ο蟀褢?yīng)對某個(gè)行為的責(zé)任委托給另外一個(gè)類了燎斩。也可以用協(xié)議定義一套接口虱歪,令某類經(jīng)由該接口獲取其所需的數(shù)據(jù)蜂绎。委托模式的這一用法旨在向類提供數(shù)據(jù),故而又稱“數(shù)據(jù)源模式”(Data Source Pattern)笋鄙。在此模式中师枣,信息從數(shù)據(jù)源(Data Source)流向類(Class);而在常規(guī)的委托模式中萧落,信息則從類流向受委托者 (Delegate)践美。
下圖演示這兩條信息流:在信息源模式中,信息從數(shù)據(jù)源流向類铐尚,而在普通的委托模式中,信息則從類流向受委托者
比方說哆姻,用戶界面框架中的“列表視圖”(list view)對象可能會(huì)通過數(shù)據(jù)源協(xié)議來獲取要在列表中顯示的數(shù)據(jù)宣增。除了數(shù)據(jù)源之外,列表視圖還有一個(gè)受委托者矛缨,用于處理用戶與列表的交互操作爹脾。將數(shù)據(jù)源協(xié)議與委托協(xié)議分離,能使接口更加清晰箕昭,因?yàn)檫@兩部分的邏輯代碼也分開了灵妨。另外,“數(shù)據(jù)源”與“受委托者”可以是兩個(gè)不同的對象落竹。然而一般情況下, 都用同一個(gè)對象來扮演這兩種角色泌霍。
要點(diǎn):
- 委托模式為對象提供了一套接口,使其可由此將相關(guān)事件告知其他對象述召。
- 將委托對象應(yīng)該支持的接口定義成協(xié)議朱转,在協(xié)議中把可能需要處理的事件定義成方法。
- 當(dāng)某對象需要從另外一個(gè)對象中獲取數(shù)據(jù)時(shí)积暖,可以使用委托模式藤为。這種情境下,該模式亦稱“數(shù)據(jù)源協(xié)議”(data source protocal)夺刑。
- 若有必要缅疟,可實(shí)現(xiàn)含有位段的結(jié)構(gòu)體,將委托對象是否能響應(yīng)相關(guān)協(xié)議方法這一信息緩存至其中遍愿。
第24條:將類的實(shí)現(xiàn)代碼分散到便于管理的數(shù)個(gè)分類之中
類中經(jīng)常容易填滿各種方法存淫,而這些方法的代碼則全部堆在一個(gè)巨大的實(shí)現(xiàn)文件里。 在此情況下沼填,可以通過Objective-C的“分類”機(jī)制纫雁,把類代碼按邏輯劃入幾個(gè)分區(qū)中。
現(xiàn)在倾哺,類的實(shí)現(xiàn)代碼按照方法分成了好幾個(gè)部分轧邪。所以說刽脖,這項(xiàng)語言特性當(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)文件很快就膨脹得無法管理了螟蝙。
- 便于調(diào)試:對于某個(gè)分類中的所有方法來說恢恼,分類名稱都會(huì)出現(xiàn)在其符號中,在調(diào)試器的回溯信息中會(huì)出現(xiàn)胰默。例如场斑,“addFriend:”方法的“符號名”(symbol name):
- [EOCPerson(Friendship) addFriend:1];
要點(diǎn):
- 使用分類機(jī)制把類的實(shí)現(xiàn)代碼劃分成易于管理的小塊。
- 將應(yīng)該視為“私有”的方法歸入名叫Private的分類中牵署,以隱藏實(shí)現(xiàn)細(xì)節(jié)漏隐。
第25條:總是為第三方類的分類名稱加前綴
分類機(jī)制通常用于向無源碼的既有類中新增功能。這個(gè)特性極為強(qiáng)大奴迅,但在使用時(shí)也很容易忽視其中可能產(chǎn)生的問題青责。這個(gè)問題在于:分類中的方法是直接添加在類里面的,它們就好比這個(gè)類中的固有方法取具。將分類方法加入類中這一操作是在運(yùn)行期系統(tǒng)加載分類時(shí)完成的爽柒。運(yùn)行期系統(tǒng)會(huì)把分類中所實(shí)現(xiàn)的每個(gè)方法都加入類的方法列表中。如果類中本來就有此方法者填,而分類又實(shí)現(xiàn)了一次浩村,那么分類中的方法會(huì)覆蓋原來那一份實(shí)現(xiàn)代碼。實(shí)際上可能會(huì)發(fā)生很多次覆蓋占哟,比如某個(gè)分類中的方法覆蓋了“主實(shí)現(xiàn)”中的相關(guān)方法心墅,而另外一個(gè)分類中的方法又覆蓋了這個(gè)分類中的方法。多次覆蓋的結(jié)果以最后一個(gè)分類為準(zhǔn)榨乎。(注:不是覆蓋怎燥,是根據(jù)selector找到的第一個(gè)方法為準(zhǔn),其后不再遍歷查找后序的同名方法)
要解決此問題蜜暑,一般的做法是:以命名空間來區(qū)別各個(gè)分類的名稱與其中所定義的方法铐姚。Obiective-C中實(shí)現(xiàn)命名空間功能,只有一個(gè)辦法,就是給相關(guān)名稱都加上某個(gè)共用的前綴隐绵。
要點(diǎn):
- 向第三方類中添加分類時(shí)之众,總應(yīng)給其名稱加上你專用的前綴。
- 向第三方類中添加分類時(shí)依许,總應(yīng)給其中的方法名加上你專用的前綴棺禾。
第26條:勿在分類中聲明屬性
屬性是封裝數(shù)據(jù)的方式(參見第6條)。盡管從技術(shù)上說峭跳,分類里也可以聲明屬性膘婶,但這種做法還是要盡量避免。原因在于蛀醉,除了 “class-continuation分類”(參見第27條)之外悬襟,其他分類都無法向類中新增實(shí)例變量,因此拯刁,它們無法把實(shí)現(xiàn)屬性所需的實(shí)例變量合成出來脊岳。
有兩種辦法可以在分類里聲明屬性:
- 把存取方法聲明為@dynamic, 也就是說,這些方法等到運(yùn)行期再提供筛璧,編譯器目前是看不見的逸绎。如果決定使用消息轉(zhuǎn)發(fā)機(jī)制(參見第12條)在運(yùn)行期攔截方法調(diào)用惹恃,并提供其實(shí)現(xiàn)夭谤,那么或許可以采用這種做法。
- 關(guān)聯(lián)對象(參見第10條)能夠解決在分類中不能合成實(shí)例變量的問題巫糙。但是在內(nèi)存管理問題上容易出錯(cuò)朗儒。
正確做法是把所有屬性都定義在主接口里。類所封裝的全部數(shù)據(jù)都應(yīng)該定義在主接口中参淹,這里是唯一能夠定義實(shí)例變量(也就是數(shù)據(jù))的地方醉锄。而屬性只是定義實(shí)例變量及相關(guān)存取方法所用的“語法糖”,所以也應(yīng)遵循同實(shí)例變量一樣的規(guī)則浙值。至于分類機(jī)制恳不,則應(yīng)將其理解為一種手段,目標(biāo)在于擴(kuò)展類的功能开呐,而非封裝數(shù)據(jù)烟勋。
雖說如此,但有時(shí)候只讀屬性還是可以在分類中使用的筐付。由于獲取方法并不訪問數(shù)據(jù)卵惦,而且屬性也不需要由實(shí)例變量來實(shí)現(xiàn)。即便在這種情況下瓦戚,也最好不要用屬性沮尿。屬性所要表達(dá)的意思是:類中有數(shù)據(jù)在支持著它。屬性是用來封裝數(shù)據(jù)的较解。某些情況下也可以直接聲明一個(gè)方法畜疾,用以獲取數(shù)據(jù)赴邻。
要點(diǎn):
- 把封裝數(shù)據(jù)所用的全部屬性都定義在主接口里。
- 在“dass-contimiation分類”之外的其他分類中庸疾,可以定義存取方法乍楚,但盡量不要定義屬性。
第27條:使用“class-continuation分類”隱藏實(shí)現(xiàn)細(xì)節(jié)
類中經(jīng)常會(huì)包含一些無須對外公布的方法及實(shí)例變量届慈。其實(shí)這些內(nèi)容也可以對外公布徒溪,并且寫明其為私有,但是這樣會(huì)泄漏實(shí)現(xiàn)細(xì)節(jié)金顿。Objective-C動(dòng)態(tài)消息系統(tǒng)(參見第11條)的工作方式?jīng)Q定了其不可能實(shí)現(xiàn)真正的私有方法或私有實(shí)例變量臊泌。然而,我們最好還是只把確實(shí)需要對外公布的那部分內(nèi)容公開揍拆,隱藏部分細(xì)節(jié)渠概。那么,這個(gè)特殊的“class-continuation分類”就派上用場了嫂拴。
“class-continuation分類”和普通的分類不同播揪,它必須定義在其所接觸的那個(gè)類的實(shí)現(xiàn)文件里。其重要之處在于筒狠,這是唯一能聲明實(shí)例變量的分類猪狈,而且此分類沒有特定的實(shí)現(xiàn)文件,其中的方法都應(yīng)該定義在類的主實(shí)現(xiàn)文件里辩恼。與其他分類不同雇庙,“class-continuation分類”沒有名字。
為什么需要有這種分類呢灶伊?因?yàn)槠渲锌梢远x方法和實(shí)例變量疆前。為什么能在其中定義方法和實(shí)例變量呢?只因有“穩(wěn)固的ABI”這一機(jī)制(第6條詳解了此機(jī)制)聘萨,使得我們無須知道對象大小即可使用它竹椒。由于類的使用者不一定需要知道實(shí)例變量的內(nèi)存布局,所以米辐,它們也就未必非得定義在公共接口中了胸完。基于上述原因儡循,我們可以像在類的實(shí)現(xiàn)文件里那樣舶吗,于 “ class-contimiatiori分類”中給類新增實(shí)例變量。
實(shí)例變量也可以定義在實(shí)現(xiàn)塊里择膝,從語法上說誓琼,這與直接添加到“class-continuation 分類”等效,只是看個(gè)人喜好了。筆者喜歡將其添加在“dass-continuation分類”中腹侣。這些實(shí)例變量并非真的私有叔收,因?yàn)樵谶\(yùn)行期總可以調(diào)用某些方法繞過此限制,不過傲隶,從一般意義上來說饺律,它們還是私有的。
最后還要講一種用法:比方說跺株,EOCPerson遵從了名為EOCSecretDelegate的協(xié)議复濒。聲明在公共接口里。你可能會(huì)說乒省,只需要向前聲明EOCSecretDelegate協(xié)議就可以不引入它了(或者說巧颈,不引入定義該協(xié)議的頭文件了)。用下面這行向前聲明語句來取代#import指令:
@protocol EOCSecretDelegate;
但是這樣一來袖扛,只要引入EOCPerson頭文件的地方砸泛,由于編譯器看不到協(xié)議的定義,所以無法得知其中所含的方法蛆封,于是編譯器會(huì)給出瞥告信息唇礁。
要點(diǎn):
- 通過“class-continuation分類”向類中新增實(shí)例變量。
- 如果某屬性在主接口中聲明為“只讀”惨篱,而類的內(nèi)部又要用設(shè)置方法修改此屬性盏筐,那 么就在“class-continuation分類”中將其擴(kuò)展為“可讀寫”。
- 把私有方法的原型聲明在“class-continuation分類”里面妒蛇。
- 若想使類所遵循的協(xié)議不為人所知机断,則可于“class-continuation分類”中聲明楷拳。
第28條:通過協(xié)議提供匿名對象
協(xié)議定義了一系列方法绣夺,遵從此協(xié)議的對象應(yīng)該實(shí)現(xiàn)它們(如果這些方法不是可選的, 那么就必須實(shí)現(xiàn))。于是欢揖,我們可以用協(xié)議把自己所寫的API之中的實(shí)現(xiàn)細(xì)節(jié)隱藏起來陶耍,將返回的對象設(shè)計(jì)為遵從此協(xié)議的純id類型。這樣的話她混,想要隱藏的類名就不會(huì)出現(xiàn)在API之中了烈钞。若是接口背后有多個(gè)不同的實(shí)現(xiàn)類,而你又不想指明具體使用哪個(gè)類坤按,那么可以考慮用這個(gè)辦法——因?yàn)橛袝r(shí)候這些類可能會(huì)變毯欣,有時(shí)候它們又無法容納于標(biāo)準(zhǔn)的類繼承體系中,因而不能以某個(gè)公共基類來統(tǒng)一表示臭脓。
此概念經(jīng)常稱為“匿名對象"(anonymous object)酗钞,這與其他語言中的“匿名對象”不同, 在那些語言中,該詞是指以內(nèi)聯(lián)形式所創(chuàng)建出來的無名類砚作,而此詞在Objective-C中則不是這個(gè)意思窘奏。第23條解釋了委托與數(shù)據(jù)源對象,其中就曾用到這種匿名對象葫录。例如着裹,在定義 “受委托者”(delegate)這個(gè)屬性時(shí),可以這樣寫:
@property {nonatomic, weak) id <EOCDelegate> delegate;
由于該屬性的類型是id <EOCDelegate>
,所以實(shí)際上任何類的對象都能充當(dāng)這一屬性, 即便該類不繼承自NSObject也可以米同,只要遵循EOCDelegate協(xié)議就行骇扇。對于具備此屬性的類來說,delegate就是“匿名的”(ammymous)面粮。如有需要匠题,可在運(yùn)行期査出此對象所屬的類型(參見第14條)。然而這樣做不太好但金,因?yàn)橹付▽傩灶愋蜁r(shí)所寫的那個(gè)EOCDelegate契約已經(jīng)表明此對象的具體類型無關(guān)緊要了韭山。
NSDictionary也能實(shí)際說明這一概念。在字典中冷溃,鍵的標(biāo)準(zhǔn)內(nèi)存管理語義是“設(shè)置時(shí)拷貝”钱磅,而值的語義則是“設(shè)置時(shí)保留”。因此似枕,在可變版本的字典中盖淡,設(shè)置鍵值對所用的方法的簽名是:
- (void)setObject:(id)object forKey:(id<NSCopying>)key;
表示鍵的那個(gè)參數(shù)其類型為id<NSCopying>
,作為參數(shù)值的對象,它可以是任何類型凿歼, 只要遵從NSCopying協(xié)議就好褪迟,這樣的話,就能向該對象發(fā)送拷貝消息了答憔。這個(gè)key參數(shù)可以視為匿名對象味赃。與delegate屬性一樣,字典也不關(guān)心key對象所屬的具 體類虐拓,而且它也決不應(yīng)該依賴于此心俗。字典對象只要能確定它可以給此實(shí)例發(fā)送拷貝消息就行了。
有時(shí)對象類型并不重要蓉驹,重要的是對象有沒有實(shí)現(xiàn)某些方法城榛,在此情況下,也可以用這些“匿名類型”(anonymous type)來表達(dá)這一概念态兴。即便實(shí)現(xiàn)代碼總是使用固定的類狠持,你可能還是會(huì)把它寫成遵從某協(xié)議的匿名類型,以表示類型在此處并不重要瞻润。
要點(diǎn):
- 協(xié)議可在某種程度上提供匿名類型喘垂。具體的對象類型可以淡化成遵從某協(xié)議的id類型献汗,協(xié)議里規(guī)定了對象所應(yīng)實(shí)現(xiàn)的方法。
- 使用匿名對象來隱藏類型名稱(或類名)王污。
- 如果具體類型不重要罢吃,重要的是對象能夠響應(yīng)(定義在協(xié)議里的)特定方法,那么可使用匿名對象來表示昭齐。