第四章 協(xié)議與分類

第四章 協(xié)議與分類

Objective-C的“協(xié)議”(protocol)與java的“接口”類似。Objective-C不支持多重繼承,因而我們把某個(gè)類應(yīng)該實(shí)現(xiàn)的一系列方法定義在協(xié)議里。協(xié)議最常見的用途是實(shí)現(xiàn)委托模式。
分類(Category)也是Objective-C的一項(xiàng)重要語言特性。利用分類機(jī)制,我們無需繼承子類即可直接為當(dāng)前類添加方法。由于Objective-C運(yùn)行期系統(tǒng)是高度動(dòng)態(tài)的,所以才能支持這一特性,然而,其中也隱藏著一些陷阱,因此在使用分類之前,應(yīng)該先理解它。

23.通過委托與數(shù)據(jù)源協(xié)議進(jìn)行對(duì)象間通信

Objective-C開發(fā)者廣泛使用一種名叫“委托模式”(Delegate pattern)的編程設(shè)計(jì)模式來實(shí)現(xiàn)對(duì)象間的通信,該模式的主旨是:定義一套接口,某對(duì)象若想接受另一個(gè)對(duì)象的委托,則需遵從此接口,以便成為其“委托對(duì)象”(delegate)。而這“另一個(gè)對(duì)象”則可以給其委托對(duì)象回傳一些信息,也可以在發(fā)生相關(guān)事件時(shí)通知委托對(duì)象。在OObjective-C中,一般通過“協(xié)議”這項(xiàng)語言特性來實(shí)現(xiàn)此模式,整個(gè)Cocoa系統(tǒng)框架都是這么做的。

舉個(gè)例子,假設(shè)要編寫一個(gè)從網(wǎng)上獲取數(shù)據(jù)的類。此類也許要從遠(yuǎn)程服務(wù)器的某個(gè)資源里獲取數(shù)據(jù)。那個(gè)遠(yuǎn)程服務(wù)器可能過很長時(shí)間才會(huì)應(yīng)答,而在獲取數(shù)據(jù)的過程中阻塞應(yīng)用程序則是一種非常糟糕的做法。于是,在這種情況下,我們通常會(huì)使用委托模式:獲取網(wǎng)絡(luò)數(shù)據(jù)的類含有一個(gè)“委托對(duì)象”,在獲取完數(shù)據(jù)之后,它會(huì)回調(diào)這個(gè)委托對(duì)象。下圖演示了回調(diào)委托對(duì)象的流程:

回調(diào)委托對(duì)象的流程

利用協(xié)議機(jī)制,很容易就能以O(shè)bjective-C代碼實(shí)現(xiàn)此模式。上圖演示的這種情況下,協(xié)議可以這樣來定義:

@protocol EOCNetworkFetcherDelegate <NSObject>
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
       didReceiveData:(NSData *)data;

-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
     didFailWithError:(NSError *)error;

@end

委托協(xié)議名通常是在相關(guān)類名后面加上Delegate一詞,整個(gè)類名采用“駝峰法”來寫。

有了這個(gè)協(xié)議之后,類就可以用一個(gè)屬性來存放其委托對(duì)象了。在本例中,這個(gè)類就是EOCNetworkFetcher,于是,此類的接口可以寫成這樣:

@interface EOCNetworkFetcher : NSObject
@property(nonatomic,weak)id<EOCNetworkFetcherDelegate> delegate;
@end

一定要注意:這個(gè)屬性需定義成weak,而非strong,因?yàn)閮烧咧g必須為“非擁有關(guān)系”。通常情況下,扮演delegate的那個(gè)對(duì)象也要持有本對(duì)象。這樣做是為了防止造成循環(huán)引用。本類中存放的委托對(duì)象的這個(gè)屬性要么定義成weak,要么定義成unsafe_unretained。如果需要在相關(guān)對(duì)象銷毀時(shí)自動(dòng)清空,則定義成weak;若不需要自動(dòng)清空,則定義為unsafe_unretained。下圖演示了本對(duì)象與委托對(duì)象之間的所有權(quán)關(guān)系:

所有權(quán)關(guān)系圖

實(shí)現(xiàn)委托對(duì)象的辦法是聲明某個(gè)類遵從委托協(xié)議,然后把協(xié)議中想實(shí)現(xiàn)的那些方法在類里實(shí)現(xiàn)出來。某類若要遵從委托協(xié)議,可以在其接口中聲明,也可以在“calss-continuation分類”中聲明。如果要向外界公布此類實(shí)現(xiàn)了某協(xié)議,那么就在接口中聲明,而如果這個(gè)協(xié)議是個(gè)委托協(xié)議的話,那么通常只會(huì)在類的內(nèi)部使用。一般都是在“calss-continuation分類”中聲明的:

@interface EOCDataModel ()<EOCNetworkFetcherDelegate>

@end

@implementation EOCDataModel
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
       didReceiveData:(NSData *)data{
    /* Handle data */
}

-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
     didFailWithError:(NSError *)error{
    /* Handle error */
}

@end

委托協(xié)議中的方法一般都是“可選的”(optional),因?yàn)榘缪荨笆芪姓摺苯巧倪@個(gè)對(duì)象未必關(guān)心其中的所有方法。為了指明可選方法,委托協(xié)議經(jīng)常使用@optional關(guān)鍵字來標(biāo)注其大部分或全部的方法:

@protocol EOCNetworkFetcherDelegate <NSObject>
@optional
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
       didReceiveData:(NSData *)data;

-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
     didFailWithError:(NSError *)error;

@end

** 如果要在委托對(duì)象上調(diào)用可選方法,那么必須提前使用類型信息查詢方法判斷這個(gè)委托對(duì)象能否響應(yīng)相關(guān)選擇子。**以EOCNetworkFetcher為例,應(yīng)該這樣寫:

NSData *data = /* data obtained from network */;
if([_delegate respondsToSelector:
    @selector(networkFetcher:didReceiveData:)]){
    [_delegate networkFetcher:self didReceiveData:data];
}

這段代碼用“respondsToSelector:”來判斷委托對(duì)象是否實(shí)現(xiàn)了相關(guān)方法。如果實(shí)現(xiàn)了,就調(diào)用,如果沒實(shí)現(xiàn),就不執(zhí)行任何操作。這樣的話,delegate對(duì)象就可以完全按照其需要來實(shí)現(xiàn)委托協(xié)議中的方法了,不用擔(dān)心因?yàn)槟膫€(gè)方法沒實(shí)現(xiàn)而導(dǎo)致程序出問題。即便沒有設(shè)置委托對(duì)象,程序也能照常運(yùn)行,因?yàn)榻onil發(fā)送消息將使if語句的值成為false。

delegate對(duì)象中的方法名也一定要起得很恰當(dāng)才行。方法名應(yīng)該準(zhǔn)確描述當(dāng)前發(fā)生的事件以及delegate對(duì)象為何要獲知此事件。在本例中,delegate對(duì)象里的方法名讀起來非常清晰,表明某個(gè)“網(wǎng)絡(luò)數(shù)據(jù)獲取器”對(duì)象剛剛接收到某份數(shù)據(jù)。正如上一段代碼所示,在調(diào)用delegate對(duì)象中的方法時(shí),總是應(yīng)該把發(fā)起委托的實(shí)例也一并傳入方法中,這樣,delegate對(duì)象在實(shí)現(xiàn)相關(guān)方法時(shí),就能根據(jù)傳入的實(shí)例分別執(zhí)行不同的代碼了。比方說可以這樣寫:

-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
       didReceiveData:(NSData *)data{
    if(fetcher==_myFetcherA){
        /* Handle data */
    }else if (fetcher==_myFetcherB){
        /* Handle data */
    }
}

delegate里的方法也可以用于從獲取委托對(duì)象中獲取信息。比方說,EOCNetworkFetcher類也許想提供一種機(jī)制:在獲取數(shù)據(jù)時(shí)如果遇到了“重定向”,那么將詢問其委托對(duì)象是否應(yīng)該發(fā)生重定向。delegate對(duì)象中的相關(guān)方法可以寫成這樣:

-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
    shouldFollowRedirectToURL:(NSURL *)url;

也可以用協(xié)議定義一套接口,令某類經(jīng)由該接口獲取其所需的數(shù)據(jù),委托模式的這一用法旨在向類提供數(shù)據(jù),故而又稱“數(shù)據(jù)源模式”(Data Source Pattern)。在此模式中,信息從數(shù)據(jù)源流向類;而在常規(guī)的委托模式中,信息則從類流向受委托者。
下圖演示了這兩條信息流:

兩條信息流流向

比方說,用戶界面框架中的“列表視圖”對(duì)象可能會(huì)通過數(shù)據(jù)源協(xié)議來獲取要在列表中顯示的數(shù)據(jù)。除了數(shù)據(jù)源之外,列表視圖還有一個(gè)受委托者,用于處理用戶與列表的交互操作。將數(shù)據(jù)源協(xié)議與委托協(xié)議分離,能使接口更加清晰,因?yàn)檫@兩部分的邏輯代碼也分開了。另外,“數(shù)據(jù)源”與“受委托者”可以是兩個(gè)不同的對(duì)象。然而一般情況下,都用同一個(gè)對(duì)象來扮演這兩種角色。

在實(shí)現(xiàn)委托模式與數(shù)據(jù)源模式時(shí),如果協(xié)議中的方法是可選的,那么就會(huì)寫出一大批類似下面這樣的代碼來:

if([_delegate respondsToSelector:@selector(someClassDidSomething)]){
        [_delegate someClassDidSomething];
    }

很容易用代碼查出某個(gè)委托對(duì)象是否能響應(yīng)特定的選擇子,可是如果頻繁執(zhí)行此操作的話,那么除了第一次監(jiān)測的結(jié)果有用之外,后續(xù)的監(jiān)測可能都是多余的。如果委托對(duì)象本身沒變,那么不太可能會(huì)突然響應(yīng)某個(gè)原來不能響應(yīng)的選擇子,也不太會(huì)突然無法響應(yīng)某個(gè)原來可以響應(yīng)的選擇子。鑒于此,我們通常把委托對(duì)象能否響應(yīng)某個(gè)協(xié)議方法這一信息緩存起來,以優(yōu)化程序效率。假設(shè)在“網(wǎng)絡(luò)數(shù)據(jù)獲取器”那個(gè)例子中,delegate對(duì)象所遵從的協(xié)議里有個(gè)表示數(shù)據(jù)獲取進(jìn)度的回調(diào)方法,每當(dāng)數(shù)據(jù)獲取有進(jìn)度時(shí),委托對(duì)象就會(huì)得到通知。這個(gè)方法在網(wǎng)絡(luò)數(shù)據(jù)獲取器的生命周期里會(huì)多次調(diào)用,如果每次都檢查委托對(duì)象是否能響應(yīng)此選擇子,那就顯得多余了。
擴(kuò)充之后的delegate:

@protocol EOCNetworkFetcherDelegate <NSObject>
@optional
-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
       didReceiveData:(NSData *)data;

-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
     didFailWithError:(NSError *)error;

-(void)networkFetcher:(EOCNetworkFetcher *)fetcher
    didUpdateProgressTo:(float)progress;


@end

將方法響應(yīng)能力緩存起來的最佳途徑是使用“位段”(bitfield)數(shù)據(jù)類型。這是一項(xiàng)乏人問津的C語言特性,但在此處用起來卻正合適。我們可以把結(jié)構(gòu)體摸個(gè)字段所占用的二進(jìn)制位個(gè)數(shù)設(shè)為特定的值。比如像這樣:

struct data{
    unsigned int fieldA:8;
    unsigned int fieldB:4;
    unsigned int fieldC:2;
    unsigned int fieldD:1;
};

在結(jié)構(gòu)體中,fieldA位段占用8個(gè)二進(jìn)制位,filedB占用4個(gè),fieldC占用2個(gè),fieldD占用1個(gè)。于是,fieldA可以表示0至255之間的值,而filedD則可以表示0或1這兩個(gè)值。我們可以像fieldD這樣,把委托對(duì)象是否實(shí)現(xiàn)了協(xié)議中的相關(guān)方法這一信息緩存起來。如果創(chuàng)建的結(jié)構(gòu)體中只有大小為1的位段,那么就能把許多Boolean值塞入一小塊數(shù)據(jù)里面了。以網(wǎng)絡(luò)數(shù)據(jù)獲取器為例,可以在該實(shí)例中嵌入一個(gè)含有位段的結(jié)構(gòu)體作為其實(shí)例變量,而結(jié)構(gòu)體中的每個(gè)位段則表示delegate對(duì)象是否實(shí)現(xiàn)了協(xié)議中的相關(guān)方法。此結(jié)構(gòu)體的用法如下:

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

這個(gè)結(jié)構(gòu)體用來緩存委托對(duì)象是否能響應(yīng)特定的選擇子。實(shí)現(xiàn)緩存功能所用的代碼可以寫在delegate屬性所對(duì)應(yīng)的設(shè)置方法里:

-(void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate
{
    _delegate = delegate;
    _delegateFlags.didReceiveData = [delegate respondsToSelector:
                                     @selector(networkFetcher:didReceiveData:)];
    _delegateFlags.didFailWithError = [delegate respondsToSelector:
                                       @selector(networkFetcher:didFailWithError:)];
    _delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:
                                          @selector(networkFetcher:didUpdateProgressTo:)];
}
```
這樣的話,每次調(diào)用delegate的相關(guān)方法之前,就不用檢測委托對(duì)象是否能響應(yīng)給定的選擇子了,而是直接判斷查詢結(jié)構(gòu)體里的標(biāo)志:
```
if(_delegateFlags.didUpdateProgressTo){
        [_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
    }
```
在相關(guān)方法要調(diào)用很多次時(shí),值得進(jìn)行這種優(yōu)化。是否需要優(yōu)化,則應(yīng)依照具體代碼來定。如果要頻繁通過數(shù)據(jù)源協(xié)議從數(shù)據(jù)源中獲得多份相互獨(dú)立的數(shù)據(jù),那么這項(xiàng)優(yōu)化技術(shù)極有可能會(huì)提高程序效率。

**要點(diǎn):**
* **委托模式為對(duì)象提供了一套接口,使其可由此將相關(guān)事件告知其他對(duì)象。**
* **將委托對(duì)象應(yīng)該支持的接口定義成協(xié)議,在協(xié)議中把可能需要處理的事件定義成方法。**
* **當(dāng)某對(duì)象需要從另外一個(gè)對(duì)象中獲取數(shù)據(jù)時(shí),可以使用委托模式。這種情況下,該模式亦稱“數(shù)據(jù)源協(xié)議”。**
* **若有必要,可實(shí)現(xiàn)含有位段的結(jié)構(gòu)體,將委托對(duì)象是否能響應(yīng)相關(guān)協(xié)議方法這一信息緩存至其中。**



## 24.將類的實(shí)現(xiàn)代碼分散到便于管理的數(shù)個(gè)分類之中
之所以要將類代碼打散到分類中還有個(gè)原因,就是便于調(diào)試:對(duì)于某個(gè)分類中的所有方法來說,分類名稱都會(huì)出現(xiàn)在其符號(hào)中。例如,“addFriend:”方法的“符號(hào)名”(symbol name)如下:
```
-\[ EOCPerson(Friendship) addFriend:\]
```
在調(diào)試器的回溯信息中,會(huì)看到類似下面這樣的內(nèi)容:

```
frame #2:0x0001c50 Test’- \[EOCPerson(Friendship) addFriend: \]
+ 32 at main.m:46
```

在編寫準(zhǔn)備分享給其他開發(fā)者使用的程序庫時(shí),可以考慮創(chuàng)建Private分類。經(jīng)常會(huì)遇到這樣一些方法:他們不是公共API的一部分,然而卻非常適合在程序庫之內(nèi)使用。此時(shí)應(yīng)該創(chuàng)建Private分類,如果程序庫中的某個(gè)地方要用到這些方法,那就引入此分類的頭文件。而分類的頭文件并不隨程序庫一并公開,于是該庫的使用者也就不知道庫里還有這些私有方法了。

**要點(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)。

比方說,要給NSString添加分類,并在其中提供一些輔助方法,用于處理與HTTP URL有關(guān)的字符串。你可能會(huì)把分類寫成這樣:
```
@interface NSString (HTTP)

//Encode a string with URL encoding
-(NSString*)urlEncodedString;

//Decode a URL encoded string
-(NSString*)urlDecodedString;

@end
```

現(xiàn)在看起來沒什么問題,可是,如果還有一個(gè)分類也往NSString里添加方法,那會(huì)如何呢?那個(gè)分類里可能也有個(gè)名叫urlEncodedString的方法,其代碼與你所添加的大同小異,但卻不能正確實(shí)現(xiàn)你所需的功能。那個(gè)分類的加載時(shí)機(jī)如果晚于你所寫的這個(gè)分類,那么其代碼就是把你的那份覆蓋掉,這樣的話,你在代碼中調(diào)用urlEncodedString方法時(shí),實(shí)際執(zhí)行的是那個(gè)分類里的實(shí)現(xiàn)代碼。由于其執(zhí)行結(jié)果和你預(yù)期的值不同,所以自己所寫的那些代碼也許就無法正常運(yùn)行了。這種bug很難追查,因?yàn)槟憧赡芤庾R(shí)不到實(shí)際執(zhí)行的urlEncodedString代碼并不是自己實(shí)現(xiàn)的那一份。

要解決此問題,一般的做法是:以命名空間來區(qū)別各個(gè)分類的名稱與其中所定義的方法。想在Objective-C中實(shí)現(xiàn)命名空間功能,只有一個(gè)辦法,就是給相關(guān)名稱都加上某個(gè)共用的前綴。與給類名加前綴時(shí)所應(yīng)考慮的因素類似,給分類所加的前綴也要選得恰當(dāng)才行。一般來說,這個(gè)前綴應(yīng)該與應(yīng)用程序或程序庫中其他地方所用的前綴相同。
比如:
```
@interface NSString (ABC_HTTP)

//Encode a string with URL encoding
-(NSString*)abc_urlEncodedString;

//Decode a URL encoded string
-(NSString*)abc_urlDecodedString;

@end
```

**要點(diǎn):**
* **向第三方類中添加分類時(shí),總應(yīng)給其名稱加上你專用的前綴。**
* **向第三方類中添加分類時(shí),總應(yīng)給其中的方法名加上你專用的前綴。**


## 26.勿在分類中聲明屬性

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

比方說一個(gè)表示個(gè)人信息的類,你決定用分類機(jī)制將其代碼分段。那么你可能會(huì)設(shè)計(jì)一個(gè)專門處理交友事務(wù)的分類,其中所有方法都與操作某人的朋友列表有關(guān)。若是不知道剛才講的那個(gè)問題,可能會(huì)把代表朋友列表的那項(xiàng)屬性也放到Friendship分類里面去了:

```
@interface EOCPerson : NSObject
@property(nonatomic,copy,readonly)NSString *firstName;
@property(nonatomic,copy,readonly)NSString *lastName;

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

@end


@interface EOCPerson (Friendship)
@property(nonatomic,strong)NSArray *firends;
-(BOOL)isFriendWith:(EOCPerson *)person;
@end
```

編譯器就會(huì)警告:
```
Property ‘friends’ requires method ‘friends’ to be defined - use @dynamic or provide a method implementation in this category
Property ‘friends’ requires method ‘setFriends:’ to be defined -use @dynamic or provide a method implementation in this category
```
這段警告意思是說此分類無法合成與friends屬性相關(guān)的實(shí)例變量,所以開發(fā)者需要在分類中為該屬性實(shí)現(xiàn)存取方法。此時(shí)可以把存取方法聲明為@dynamic,也就是說,這些方法等到運(yùn)行期再提供,編譯器目前是看不見的。如果決定使用消息轉(zhuǎn)發(fā)機(jī)制(12條)在運(yùn)行期攔截方法調(diào)用,并提供其實(shí)現(xiàn),那么或許可以采用這種做法。

關(guān)聯(lián)對(duì)象(第10條)能夠解決在分類中不能合成實(shí)例變量的問題。比方說,我們可以在分類中用下面這段代碼實(shí)現(xiàn)存取方法:
```
#import <objc/runtime.h>
static const char *kFriendsPropertyKey = "kFriendsPropertyKey";

@implementation EOCPerson(Friendship)
-(NSArray *)firends{
    return objc_getAssociatedObject(self, kFriendsPropertyKey);
}

-(void)setFirends:(NSArray *)firends{
    objc_setAssociatedObject(self,
                             kFriendsPropertyKey,
                             firends,
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
```

這樣做可行,但不太理想。要把相似的代碼寫很多遍,而且在內(nèi)存管理問題上容易出錯(cuò),因?yàn)槲覀冊跒閷傩詫?shí)現(xiàn)存取方法時(shí),經(jīng)常會(huì)忘記遵從其內(nèi)存管理語義。比方說,你可能通過屬性特質(zhì)修改了某個(gè)屬性的內(nèi)存管理語義。而此時(shí)還要記得,在設(shè)置方法中也得修改設(shè)置關(guān)聯(lián)對(duì)象時(shí)所用的內(nèi)存管理語義才行。所以說,盡管這個(gè)做法不壞,但筆者不推薦。

此外,你可能會(huì)選用可變數(shù)組來實(shí)現(xiàn)friends屬性所對(duì)應(yīng)的實(shí)例變量。若是這么做,就得在設(shè)置方法中將傳入的數(shù)組參數(shù)拷貝為可變版本,而這又成為另外一個(gè)編碼時(shí)容易出錯(cuò)的地方。因此,把屬性定義在“主接口”(main interface)中要比定義在分類里清晰的多。

在本例中,正確的做法是把所有屬性都定義在主接口里。類所封裝的全部數(shù)據(jù)都應(yīng)該定義在主接口中,這里是唯一能夠定義實(shí)例變量(也就是數(shù)據(jù))的地方。而屬性只是定義實(shí)例變量及相關(guān)存取方法所用的“語法糖”,所以也應(yīng)遵循同實(shí)例變量一樣的規(guī)則。至于分類機(jī)制,則應(yīng)將其理解為一種手段,目標(biāo)在于擴(kuò)展類的功能,而非封裝數(shù)據(jù)。

雖說如此,但有時(shí)候只讀屬性還是可以在分類中使用的。比方說,要在NSCalendar類中創(chuàng)建分類,以返回包含各個(gè)月份名稱的字符串?dāng)?shù)組。由于獲取方法并不訪問數(shù)據(jù),而且屬性也不需要由實(shí)例變量來實(shí)現(xiàn),所以可像下面這樣來實(shí)現(xiàn)此分類:

```
@interface NSCalendar (EOC_Additions)
@property(nonatomic,strong,readonly)NSArray *eoc_allMonths;

@end

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

@end
```

由于實(shí)現(xiàn)屬性所需的全部方法(在本例中,屬性是只讀的,所以只需實(shí)現(xiàn)一個(gè)方法)都已實(shí)現(xiàn),所以不會(huì)再為該屬性自動(dòng)合成實(shí)例變量了。于是,編譯器就不會(huì)發(fā)出警告信息。然而,即便在這種情況下,也最好不要用屬性。屬性所要表達(dá)的意思是:類中有數(shù)據(jù)在支持著它。屬性是用來封裝數(shù)據(jù)的。在本例中,應(yīng)該聲明一個(gè)方法,用以獲取月份名稱列表:
```
@interface NSCalendar (EOC_Additions)
-(NSArray *)eoc_allMonths;
@end
```

**要點(diǎn):**
* **把封裝數(shù)據(jù)所用的全部屬性都定義在主接口里。**
* **在“class-continuation分類”之外的其他分類中,可以定義存取方法,但盡量不要定義屬性。**


## 27.使用“class-continuation分類”隱藏實(shí)現(xiàn)細(xì)節(jié)

“class-continuation分類”和普通的分類不同,它必須定義在其所接續(xù)的那個(gè)類的實(shí)現(xiàn)文件里。其重要之處在于,這是唯一能聲明實(shí)例變量的分類,而且此分類沒有特定的實(shí)現(xiàn)文件,其中的方法都應(yīng)該定義在主實(shí)現(xiàn)文件里。與其他分類不同,“class-continuation分類”沒有名字。

為什么需要有這種分類呢?因?yàn)槠渲锌梢远x方法和實(shí)例變量。為什么能在其中定義方法和實(shí)例變量呢?只因?yàn)橛小胺€(wěn)固的ABI”(第6條),使得我們無須知道對(duì)象大小即可使用它。由于類的使用者不一定需要知道實(shí)例變量的內(nèi)存布局,所以,它們也就未必非得定義在公共接口中了?;谏鲜鲈?,我們可以像在類的實(shí)現(xiàn)文件里那樣,于“class-continuation分類”中給類新增實(shí)例變量。只需要在適當(dāng)位置上多寫幾個(gè)括號(hào),然后把實(shí)例變量放進(jìn)去:

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

@implementation EOCPerson
{
    int _anotherInstanceVariable;
}
//Method implementions here
@end
```
公共接口里本來就能定義實(shí)例變量。不過,把它們定義在“class-continuation分類”或“實(shí)現(xiàn)塊”中可以將其隱藏起來,只供本類使用。即便在公共接口里將其標(biāo)注為private,也還是會(huì)泄露實(shí)現(xiàn)細(xì)節(jié)。比方說,你有個(gè)絕密的類,不想給其他人知道。假設(shè)你縮寫的某個(gè)分類擁有那個(gè)絕密類的實(shí)例,而這個(gè)實(shí)例變量又聲明在公共接口里面:

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

@class EOCSuperSecretClass;

@interface EOCClass : NSObject
{
    EOCSuperSecretClass *_secretInstance;
}
@end
```
那么,信息就泄露了,別人就會(huì)知道有個(gè)名叫EOCSuperSecretClass的類。為解決此問題,可以不把實(shí)例變量聲明為強(qiáng)類型,而是將其類型由EOCSuperSecretClass* 改為id。然而這么做不夠好,因?yàn)樵陬惖膬?nèi)部使用此實(shí)例時(shí),無法得到編譯器的幫助。沒必要只因?yàn)橄雽?duì)外界隱藏某個(gè)內(nèi)容就放棄編譯器的輔助檢查功能吧?這個(gè)問題可以由“class-continuation分類”來解決。那個(gè)代表絕密類的實(shí)例可以聲明成這樣:

```
#import "EOCClass.h"
#import "EOCSuperSecretClass.h"

@interface EOCClass ()
{
    EOCSuperSecretClass *_secretInstance;
}
@end

@implementation EOCClass
//Methods here
@end

```
實(shí)例變量也可以定義在實(shí)現(xiàn)塊里,從語法上說,這與直接添加到“class-continuation分類”等效,只是看個(gè)人喜好了。這些實(shí)例變量并非真的私有,因?yàn)樵谶\(yùn)行期總可以調(diào)用某些方法繞過此限制,不過,從一般意義上來說,它們還是私有的。此外,由于沒有聲明在公共頭文件里,所以將代碼作為程序庫的一部分來發(fā)行時(shí),其隱藏程度更好。



**要點(diǎn):**
* **通過“class-continuation分類”向類中新增實(shí)例變量。**
* **如果某屬性在主接口中聲明為“只讀”,而類的內(nèi)部又要用設(shè)置方法修改此屬性,那么就在“class-continuation分類”中將其擴(kuò)展為“可讀寫”。**
* **把私有方法的原型聲明在“class-continuation分類”里面。**
* **若想使類所遵循的協(xié)議不為人所知,則可于“class-continuation分類”中聲明。**



## 28.通過協(xié)議提供匿名對(duì)象

協(xié)議定義了一系列方法,遵從此協(xié)議的對(duì)象應(yīng)該實(shí)現(xiàn)它們(如果這些方法不是可選的,那么就必須實(shí)現(xiàn))。于是,我們可以用協(xié)議把自己所寫的API之中的實(shí)現(xiàn)細(xì)節(jié)隱藏起來,將返回的對(duì)象設(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)常稱為“匿名對(duì)象(anonymous object)”,這與其他語言中的“匿名對(duì)象”不同,在那些語言中,該詞是指以內(nèi)聯(lián)形式所創(chuàng)建出來的無名類,而此詞在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ì)于具備此屬性的類來說,delegate就是“匿名的”。如有需要,可在運(yùn)行期查出此對(duì)象所屬的類型。然而這樣做不太好,因?yàn)橹付▽傩灶愋蜁r(shí)所寫的那個(gè)EOCDelegate契約已經(jīng)表明此對(duì)象的具體類型無關(guān)緊要了。

NSDictionary也能實(shí)際說明這一概念。在字典中,鍵的標(biāo)準(zhǔn)內(nèi)存管理語義是“設(shè)置時(shí)拷貝”,而值的語義則是“設(shè)置時(shí)保留”。因此,在可變版本的字典中,設(shè)置鍵值對(duì)所用的方法的簽名是:
```
- (void)setObject:(id)anObject forKey:(id <NSCopying>)aKey
```
表示鍵的那個(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ù)據(jù)庫連接的程序庫也用這個(gè)思路,以匿名對(duì)象來表示從另一個(gè)庫中所返回的對(duì)象。對(duì)于處理連接所用的那個(gè)類,你也許不想叫外人知道其名字,因?yàn)椴煌臄?shù)據(jù)庫可能要用不同的類來處理。如果沒辦法令其都繼承自同一基類,那么就得返回id類型的東西了。不過我們可以把所有數(shù)據(jù)庫連接都具備的那些方法放在協(xié)議中,令返回的對(duì)象遵從此協(xié)議。協(xié)議可以這樣寫:
```
@protocol EOCDatabaseConnection <NSObject>

-(void)connect;
-(void)disconnect;
-(BOOL)isConnected;
-(NSArray *)performQuery:(NSString *)query;

@end
```
然后,就可以用“數(shù)據(jù)庫處理器”(database handler)單例來提供數(shù)據(jù)庫連接了。這個(gè)單例的接口可以寫成:
```
#import <Foundation/Foundation.h>

@protocol EOCDatabaseConnection;

@interface EOCDatabaseManager : NSObject
+(id)shareInstance;
-(id<EOCDatabaseConnection>)connectionWithIdentifier:(NSString *)identiier;
@end
```
這樣的話,處理數(shù)據(jù)庫連接所用的類的名稱就不會(huì)泄露了,有可能來自不同框架的那些類現(xiàn)在均可以經(jīng)由同一個(gè)方法來返回了。使用此API的人僅僅要求所返回的對(duì)象能用來連接、斷開并查詢數(shù)據(jù)庫即可。

有時(shí)對(duì)象類型并不重要,重要的是對(duì)象有沒有實(shí)現(xiàn)某些方法,在此情況下,也可以用這些“匿名類型”來表達(dá)這一概念。即便實(shí)現(xiàn)代碼總是使用固定的類,你可能還是會(huì)把它寫成遵從某協(xié)議的匿名類型,以表示類型在此處并不重要。



**要點(diǎn):**
* **協(xié)議可在某種程度上提供匿名類型。具體的對(duì)象類型可以淡化成遵從某協(xié)議的id類型,協(xié)議里規(guī)定了對(duì)象所應(yīng)實(shí)現(xiàn)的方法。**
* **使用匿名對(duì)象來隱藏類型名稱(或類名)。**
* **如果具體類型不重要,重要的是對(duì)象能夠響應(yīng)(定義在協(xié)議里的)特定方法,那么可使用匿名對(duì)象來表示。**





轉(zhuǎn)載請(qǐng)注明出處:[第四章 協(xié)議與分類](http://www.itdecent.cn/p/f6ea2bf6b48d)

_參考:《Effective Objective-C 2.0》_
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容