什么是鍵值編碼
什么是鍵值編碼
訪問實例變量可以通過訪問器訪問,也可以將屬性設(shè)置為@public。
與此相對,鍵值編碼(key-value coding)是指,將表示對象包含的信息的字符串作為鍵值使用,來間接訪問該信息的方式。鍵值編碼提供了非常強大的功能,基本上,只要存在訪問器方法,聲明屬性或?qū)嵗兞浚涂梢詫⑵涿种付樽址畞碓L問。本章中,可以訪問,設(shè)定的對象狀態(tài)的值稱為屬性(property)。
之所以說鍵值編碼的訪問是間接的,是因為以下兩點:
也可以在運行中確定作為鍵的字符串。
使用者無法知道實際訪問屬性的方法。
鍵值編碼的基本處理
鍵值編碼的必須方法在非正式協(xié)議NSKeyValueCoding中聲明,這些默認在NSObject中實現(xiàn)。
有訪問器的屬性會使用該訪問器,沒有訪問器的屬性也可以設(shè)定值和訪問。而且變量obj也可以為id類型。
訪問屬性
鍵值編碼的方法的行為
下面的講述圍繞name進行。以下劃線開始的名字不能用于方法名或?qū)嵗兞棵4送?,也不要使用以get開始的名字。
接收器中如果有name訪問器則使用它。
沒有訪問器時,使用接收器的類方法accessInstanceVariablesDirectly來查詢。返回YES時,如果存在實例變量name時(或_name,isName,_isName等)則返回其值。使用引用計數(shù)的方式時,實例變量如果是對象,則舊值會被自動釋放,新值被保存并帶入。
既沒有訪問器也沒有實例變量時,將引起接收器調(diào)用方法setValue:forUndefinedKey:
應該返回的值如果不是對象,則返回被適當?shù)膶ο蟀b的值。
但是,如果接收器包含與帶索引的訪問器模式一致的方法,則將返回有數(shù)組對象行為代理(proxy)對象。
accessInstanceVariablesDirectly只要該方法返回YES,實例變量的可見屬性即使有@private修飾,也可以訪問。
setValue:forKey:如果設(shè)定值失敗,則調(diào)用下面的方法。
- (void)setValue:(id)value forUndefinedKey:(nonnullNSString *)key
//不能設(shè)置鍵字符串key對應的屬性值時,從方法setValue:forKey中調(diào)用該方法。默認情況下,該方法的執(zhí)行會觸發(fā)異常NSUndefinedKeyException。不過,通過在子類中修改定義,就可以返回其他對象。
鍵值編碼比訪問器以及實例變量名更具靈活性。
由于鍵值編碼所接收的對象都是id類型,因此,在該部分中,編譯時不會進行仔細的類型檢查。所以一定要注意不要傳入與屬性不符的對象。
使用鍵值編碼的程序如何執(zhí)行,不是由靜態(tài)解析源碼語法的結(jié)果決定的,而是使用程序運行時包含的信息動態(tài)決定的。與實例變量的可視屬性不同,有方法決定是否可以訪問實例變量這一點,會使人感覺有損一致性??傊I值編碼這一強大的功能就像一把雙刃劍,也伴隨著危險,因此不可以濫用。
屬性值的自動轉(zhuǎn)換
將屬性中單純的數(shù)值,也就是整數(shù)或?qū)崝?shù),布爾值這樣的數(shù)據(jù)稱為標量(scalar)值。將標量值,結(jié)構(gòu)體,字符串或NSNumber等常數(shù)對象稱為屬性(attribute)。
方法setValue:在返回值為標量值或結(jié)構(gòu)體時,會返回將其自動包裝的對象。另一方面,為了給setValue:forKey:傳入值,也需要使用適當?shù)膶ο髞戆b。
單純的數(shù)值用NSNumber來包裝,結(jié)構(gòu)體用NSValue類的實例來包裝。屬性值如果為對象,則可以將nil作為值傳遞。另一方面,當將nil作為值傳遞時,setNilValueForKey:方法將被發(fā)送給接收器。
- (void)setNilValueForKey:(NSString *)key
//執(zhí)行該方法將產(chǎn)生NSInvalidArgumentException異常
字典對象和鍵值編碼
字典類NSDictionary和NSMutableDictionary包含了協(xié)議NSKeyValueCoding的方法,使用它們可以進行鍵值編碼。
- (id)valueForKey:(NSString *)key
//鍵字符串開頭不是@時,將調(diào)用方法objectForKey:。如果開頭為”@“,則將去除開頭字符后剩余的字符串作為鍵,調(diào)用超類的方法valueForKey:。
NSMutableDictionary中定義了以下的方法:
- (void)setValue:(id)value forKey:(NSString *)key
//一般會調(diào)用方法setObject:forKey:,參數(shù)value為nil時,調(diào)用方法removeObjectForKey:刪除鍵對應的對象。
根據(jù)鍵路徑進行訪問
屬性為對象時,該對象還可能持有屬性。在鍵值編碼中,使用某個鍵訪問獲得某個屬性對象后,如果希望再用別的鍵來訪問該對象,可采用如下方法:
idname = [aGroup valueForKey:@“l(fā)eader.name"];
像這樣,用“.”連接鍵表示的字符串稱為鍵路徑(key path)。只要能找到對象,點和鍵多長都沒有關(guān)系。
聲明屬性的點是運算符,而這里的鍵路徑則是一個字符串。
使用鍵路徑訪問屬性的方法如下:(略)
一對一關(guān)系和一對多關(guān)系
使用鍵(或鍵路徑)訪問時,我們將對象確定為一個的屬性稱為指定一對一關(guān)系(to-one relationship)的屬性,將屬性值為數(shù)組或集合的屬性稱為指定一對多關(guān)系(to-many relationship)的屬性。
如果鍵對應一個對象,那么也是一對一關(guān)系。
關(guān)于一對多關(guān)系屬性的訪問,更改,需要留意以下幾點:
1.使用集合元素對象持有的鍵訪問一對多關(guān)系屬性時,鍵對應的屬性被作為數(shù)組或集合返回。
2.使用集合元素對象持有的鍵設(shè)定一對多關(guān)系屬性時,各元素對象鍵對應的屬性全都被更改。
數(shù)組對象和鍵值編碼
數(shù)組類NSArray和NSMutableArray以及集合類NSSet和NSMutableSet都包含協(xié)議NSKeyValueCoding的方法,也都有鍵值編碼。
- (id)valueForKey:(NSString *)key//以key為參數(shù),對集合的各元素調(diào)用方法valueForKey:后返回數(shù)組(NSSet時返回集合)。對各成員適用方法valueForKey:,返回nil時,則包含NSNull實例。
- (void)setValue:(id)value forKey:(NSString *)key//對集合各元素調(diào)用方法setValue:forKey:。需要注意的是,即使集合對象自身不可以改變,也能調(diào)用該方法。
一對多關(guān)系的訪問
帶索引的訪問器模式
即使是非數(shù)組對象,如果有某個模式的訪問器,也可以進行像數(shù)組一樣的鍵值編碼操縱。該訪問器模式稱為帶索引的訪問器模式(indexed accessor pattern)。
下面是兩個方法等實現(xiàn)。下劃線部分會輸入字符串。
- (NSUInteger)countOf___;
- (id)objectIn___AtIndex:(NSUInteger)index;
為了提高運行效率,除上述兩個方法外,還可以實現(xiàn)下面的方法。這也是和數(shù)組類簇中的getObjects:range:相同的方法。
- (void)get___:(id__unsafe_unretained[])aBuffer range:(NSRange)aRange;
一對多關(guān)系的可變訪問
獲得可變數(shù)組對象的方法。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key
//返回相當于用鍵字符串指定的一對多關(guān)系的屬性的可變數(shù)組。操作被返回的數(shù)組與操作屬性同時進行。
- (NSMutableArray *)NSMutableArrayValueForKeyPath:(NSString *)keyPath
//接收器屬性在鍵路徑中指定。
用該方法操作屬性時,除了之前提到的訪問常數(shù)數(shù)組需要添加的兩個方法之外,還需要實現(xiàn)用來插入和刪除的方法。下劃線部分加入了鍵字符串(通常為復數(shù)形式)。使用這些方法并通過鍵值編碼訪問時,內(nèi)部的代理就會作為數(shù)組進行操作了。
- (void)insertObject:(id)obj in___AtIndex:(NSUInteger)index;
- (void)removeObjectFrom___AtIndex:(NSUInteger)index;
不僅是上述兩個方法,通過實現(xiàn)下面的方法也可以實現(xiàn)屬性的可變訪問。
- (void)set___:(id)anArray;
在實現(xiàn)該方法時,通過使用被作為參數(shù)傳入的數(shù)組元素對象,就可以置換一對多關(guān)系的全部屬性內(nèi)容。
在添加了插入和刪除的方法的基礎(chǔ)上,如果能實現(xiàn)下面的方法,則將能顯著改善運行效果。
- (void)replaceObjectIn___AtIndex:(NSUInteger)index withObject:(id)obj;
如果上述方法都沒有實現(xiàn),那么當存在與鍵字符串同名的實例變量且該變量又是可變數(shù)組的對象時,方法mutableArrayValueForKey:將直接返回該值。
KVC標準
驗證屬性值
在某些情況下,如果預期之外的對象被設(shè)定了屬性值,那么就可能出現(xiàn)問題。
因此,在為某屬性帶入對象前,可以使用相應的方法來驗證,但是驗證方法不能自動調(diào)用(使用Cocoa綁定時,可以設(shè)定自動驗證),因此,在訪問屬性前,必須自行調(diào)用該方法。
驗證某鍵字符串的屬性值的方法可按如下形式定義。下劃線中寫入鍵字符串。參數(shù)ioValue為需要驗證的對象的指針。參數(shù)outError被用來當驗證結(jié)果中存在時返回出錯信息。
- (BOOL)validate___:(inoutid*)ioValue error:(outNSError **)outError;
對象有問題時,但是能將對象修正為有效值時,方法會創(chuàng)建新的對象,并取代原對象將新對象帶入ioValue。參數(shù)outError不變,返回值為YES。
對象有問題且不能修正時,則創(chuàng)建錯誤對象并將其帶入?yún)?shù)outError。方法返回NO。
設(shè)定屬性訪問值的訪問器方法(set___:)不能調(diào)用驗證方法。
運行時,鍵會被動態(tài)地賦值給對象的情況下,不能在代碼中使用上述方法名。此時,可以使用下面的兩個方法。
- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString *)key error:(outNSError**)outError
//使用指定鍵尋找validate___:error:的驗證方法并調(diào)用,如果不存在這樣的驗證方法,則返回YES。
- (BOOL)validateValue:(inoutid*)ioValue forKeyPath:(NSString *)keyPath error:(outNSError**)outError
//實際被調(diào)用的驗證方法并不是該方法的接收器,而是與最后的鍵元素相對應的屬性的驗證方法。
鍵值編碼的準則
如果可以使用鍵值編碼來訪問某個屬性,則稱該屬性是鍵值編碼的準則,或稱為KVC準則(compliance)。反之,如果知道某屬性為KVC準則,那么就可以編寫使用鍵值編碼的程序。KVC準則和協(xié)議適用的概念不同,它不是以類為單位,而是討論以各個屬性為單位是不是準則的問題。
要使某屬性為KVC準則,就必須實現(xiàn)能使用valueForKey:方法的訪問器。當屬性可變時,還需要與方法setValue:forKey:相應的訪問器。下面列舉一些具體的條件:
property為屬性(標量值或單純型的對象)或一對一關(guān)系時,要想成為KVC準則,就需要滿足如下條件。屬性名為“name”。
1.實現(xiàn)了name或isName訪問器方法?;蛘甙琻ame(或_name)實例變量。
2.可變屬性時,還需要實現(xiàn)setName:方法。需要執(zhí)行鍵值驗證時,要實現(xiàn)驗證方法(validateName:error:)。但是,setName:方法中不能調(diào)用驗證方法。
屬性為一對多關(guān)系時,要想成為KVC準則。需滿足如下條件。屬性名為“names”。
1.實現(xiàn)了返回數(shù)組的names方法。或者持有包含names(或names)數(shù)組對象的實例變量或者實現(xiàn)了帶索引的訪問器模式的方法countOfNames以及objectInNamesAtIndex
2.當一對多關(guān)系的屬性可變時
持有返回可變數(shù)組對象的names方法。或者
實現(xiàn)了帶索引的訪問器模式的方法
insertObject:inNamesAtIndex:以及removeObjectFromNamesAtIndex:。
當然,在實現(xiàn)帶索引的訪問器模式的方法時,為改善執(zhí)行效率,也可以添加其他方法來實現(xiàn)。
鍵值觀察
鍵值觀察(key-value-observing),即某個對象的屬性改變時通知其他對象的機制。有時也記作KVO。
對被視察對象來說,鍵值觀察就是注冊想要監(jiān)視的屬性的鍵路徑和觀察者。當屬性改變時,觀察者會接收到消息。
僅僅在使用鍵值編碼準則來訪問訪問器或?qū)嵗兞康那闆r下,才可以監(jiān)視屬性的變化。在方法內(nèi)直接改變實例變量值時,就不能監(jiān)視了。
NSObject中提供了鍵值觀察所必需的方法,頭文件Foundation/NSKeyValueObserving.h中將其定義為了非正式協(xié)議。
使用引用計數(shù)管理方式時需要注意一些事項。注冊屬性監(jiān)視時,不需要持有觀察者及監(jiān)視對象的屬性。還有,如果在不刪除注冊信息的情況下將關(guān)聯(lián)對象釋放,那么隨著屬性的變更,就可能會發(fā)生訪問已釋放了的對象的危險。
一對多的屬性監(jiān)視
監(jiān)視方法與前述方法相同。但有一點必須注意,那就是一對多關(guān)系為數(shù)組類型時使用方法mutableArrayValueForKey:獲得對象,集合類型同樣如此,如果不修改值是不能被監(jiān)視的。
依賴鍵的登記
某屬性值伴隨著同一對象的其他屬性的改變而改變是常有的事情。通過事先將這樣的依賴關(guān)系在類中注冊,那么即使屬性值間接的發(fā)生了變化,也會發(fā)送通知。為此,需要使用下面的類方法:(略)
Cocoa綁定描述
目標-行為-模式的弱點
在面向?qū)ο蟪绦蛟O(shè)計中,應盡可能地去除特定的類與類之間的關(guān)系,定義低耦合的類。也就是說,某個類改變時,最好不會影響其他的類,否則就不是我們期望的編程。但是,就算完美地定義了每個類,它們間如果不能聯(lián)動也就不能實現(xiàn)功能。一個典型的例子就是,窗體及窗體上面的按鈕菜單等GUI組件也是對象,它們間如果不連接,程序就不能運行。
這樣看來,除了各個類或GUI組件原本就應該有的功能之外,還需要為它們之間的聯(lián)動補充必要的代碼。而這樣的代碼就像是把元素粘在一起的膠水,因此稱為膠水代碼(glue code)。膠水代碼既可以被寫成專門的類,也可以滲透在各個關(guān)聯(lián)的類中。
Mac OSX從第一個版本NexTstep開始,就有了將GUI組件與使用組件的對象結(jié)合在一起的開發(fā)工具Interface Builder?!澳z”的部分不需要特意編寫代碼,使用Interface Builder的的GUI環(huán)境就可以簡單地實現(xiàn),因此能大幅提高編程效率。Interface Builder中中GUI組件被操作時,會預先指定向哪個對象發(fā)送什么消息,這里,Objective-C靈活的消息發(fā)送機制發(fā)揮著非常大的作用。以上就是我們說明過的目標行為模式。
但是,“從操作組件向目標發(fā)送消息”這樣的方式在很多情況下都是無能為力的。特別是當值改變時,為了使多個對象聯(lián)動,必須書寫專門的代碼。圖20-2(a)希望實現(xiàn)的是,繪圖用的參數(shù)可從滑塊或文本域輸入中獲得,并根據(jù)值的變化改變各種顯示,這樣的情況很常見,但只用Interface Builder連接解決不了這樣的問題,還有必要使用專門的對象。程序自身雖然簡單,但這樣的組合多了的話,就要寫大量相似的代碼。
什么是Cocoa綁定
從Mac OS X 10.3起開始引入的Cocoa綁定(Cocoa binding)是指,使用鍵值編碼和鍵值觀察的組合,在多個對象間共享屬性值的變化的機制。(在iOS中不可以使用)
上圖(b)為例說明了它的概要。
Cocoa綁定所需的方法
使用Cocoa綁定,將某對象綁定到控制器屬性時,該對象必須實現(xiàn)下面的方法。該方法用頭文件AppKit/NSKeyValueBinding.h中的非正式協(xié)議NSKeyValueBindingCreation來聲明。