什么是 KVO
KVO(Key Value Coding)是一種非正式協(xié)議,它提供了一種間接訪問對象屬性的方法,也就是通過字符串標(biāo)識屬性。直接訪問對象屬性的方法就是調(diào)用存取方法,或直接使用實(shí)例變量。
KVO 是比較基本的技術(shù)點(diǎn),經(jīng)常與其他技術(shù)交互使用。在使用 Cocoa 綁定、KVO、Core Data 時(shí),需要用到 KVC 技術(shù)。
存取方法,見名知意,就是用來設(shè)置和獲得對象數(shù)據(jù)模型屬性值的方法。有兩種基本的存取方法,第一種是 getter ,它返回屬性的值。第二種是 setter ,它設(shè)置屬性的值??赡苣銜械揭苫?,說我并沒有見到或者使用這些方法啊,你是因?yàn)?Foundation 已經(jīng)默認(rèn)為你實(shí)現(xiàn)了。
還是舉例說明:
@interface People : NSObject
{
NSString *_name; // 實(shí)例變量
}
@property (nonatomic, assign) NSUInteger age; // 屬性
@end
// 在程序中調(diào)用
_name = @"WellCheng";
self.age = 22;
// 上面的代碼等同于
[self setAge: 22];
_age = 22;
上面的代碼中,直接使用實(shí)例變量 _name 并給其賦值。調(diào)用 age 屬性時(shí),使用了 setter 存取器。有關(guān)更多存取器、屬性、assign 關(guān)鍵字等內(nèi)容就不做更多說明了,只要明白 KVC 跟訪問器有關(guān)就好了。
在程序中使得 KVC 兼容存取器很重要,這樣讓數(shù)據(jù)進(jìn)行了封裝又促進(jìn)了與 Cocoa 綁定、KVO 和 Core Data 的集成,并且還能顯著的減少代碼。
使用 KVC 簡化代碼
假如有這樣一個(gè)需求,在一個(gè)方法中,根據(jù)參數(shù)返回對象不同的實(shí)例變量值。
- (id)valueForPeople:(People *)p withParam:(NSString *)identifier {
if [identifier isEqualToString: @"name"] {
return p.name;
}
if [identifier isEqualToString: @"age"] {
return p.age;
}
// ...
}
如果 People 這個(gè)類有很多的屬性,那么這個(gè)方法將會變的很長。下面我們使用 KVC 簡化:
- (id)valueForPeople:People(People *)p withParam:(NSString *)identifier {
return [p valueForKey:identifier];
}
KVC 一句話搞定。贊贊噠~
KVO 基礎(chǔ)知識
Keys 和 key Paths
key 標(biāo)識對象的某個(gè)屬性,通常是存取器的方法名或者屬性名。對于 People 類的對象來說,可以是 name、age、birthdayDate 等。
Key Path 是由點(diǎn)分隔的字符串,用來獲取更深層次的屬性。假若 birthdayDate 是 Date 類型,并且 Date 類還有 year 、month、day 等屬性。那么 birthdayDate.day 就是 key path。
通俗點(diǎn)來說,key path 就是為了更加方便的獲取更深層級的屬性,如果只能獲取到對象第一層的屬性,那么 KVC 價(jià)值就不大了。
如果不使用 keyPath,可能我們的代碼會是這樣子:
[[self valueForKey:@"birthdayDate"] valueForKey:@"year"];
如果像上面只有兩層還好,如果有多層,那這代碼也太不優(yōu)雅了。試想一下,長長的一串 valueForKey --!
使用 KVC 獲取屬性值
valueForKey: 方法返回指定 key 對應(yīng)的值。如果對象中沒有該 key 對應(yīng)的存取器方法或者實(shí)例變量,對象將調(diào)用自身的valueForUndefinedKey 方法,此方法默認(rèn)的實(shí)現(xiàn)為拋出 NSUndefinedKeyException 異常,子類化此方法可覆蓋這個(gè)默認(rèn)的行為。
在實(shí)際的使用中,我們一般情況下是需要實(shí)現(xiàn)這個(gè)方法來做一些容錯(cuò)處理的。
dictionaryWithValuesForKeys: 這個(gè)方法就比較厲害了,它將返回一個(gè)字典,key 仍為傳入的 key,key 對應(yīng)的值為單獨(dú)調(diào)用 valueForKey: 的結(jié)果。
如果傳入的 key 為 nil ,也會按照 undefined 處理跑出異常,如果有需要在數(shù)組中返回 nil,需要用 NSNull 類封裝。
如果 key path 返回的值是對應(yīng)多個(gè)對象,那么將會全部返回。
使用 KVC 設(shè)置屬性值
setValue:forKey:方法設(shè)置指定 key 的值,此方法默認(rèn)對于 NSValue 封裝進(jìn)行解包,用于處理常量和結(jié)構(gòu)體。
同樣,如果 key 不存在,那么將默認(rèn)發(fā)送 setValue:forUndefinedKey: 消息,消息的默認(rèn)實(shí)現(xiàn)也是拋異常。
setValuesForKeysWithDictionary: 方法用于一組 key 的設(shè)置。
有一種情況是對于非對象的值設(shè)置為 nil,這種情況下將調(diào)用自身的 setNilValueForKey: 方法,此方法的默認(rèn)實(shí)現(xiàn)仍然是拋異常,所以如果有這種特殊的需求,需要特殊處理。
這個(gè)主要用于當(dāng)對常量或者非對象的結(jié)構(gòu)體發(fā)送了這個(gè)方法時(shí),我們將其轉(zhuǎn)換一下,比如對于 Double 類型發(fā)送 nil,按照本意就是將 Double 類型的變量置為 0 ,如果是 BOOL 類型的,置為 false 即可,具體情況具體靈活運(yùn)用。
也許你有傳入 key 為 nil 的需求,這個(gè)時(shí)候,就需要使用 NSNull 類了。KVC 會自動將 [NSNull null] 轉(zhuǎn)換為 nil 進(jìn)行調(diào)用。
點(diǎn)語法與 KVC
可能你會對于 keyPath 中的點(diǎn)語法與 self 的點(diǎn)語法有一些疑惑,其實(shí)這兩者之間沒有什么關(guān)系。
keyPath 中的點(diǎn)是用來區(qū)分元素邊界的,只是當(dāng)時(shí)恰好用點(diǎn)來分割。self 中的點(diǎn)是語法糖,為了方便而已,畢竟寫一串大括號還是很丑的。其最終仍然是方法調(diào)用。即
self.birthdayDate.year = @"1993";
// 等同于
[[self birthdayDate] year] = @"1993";
// 當(dāng)然,如果你想要使用 KVC 的方式來簡單賦值也并不是不行
// 下面的調(diào)用與上面的結(jié)果相同
[self setValue: @"1993" ForKeyPath:@"birthdayDate.year"];
KVC 與存取方法
為了讓 KVC 能夠準(zhǔn)確找到存取方法,你需要實(shí)現(xiàn) KVC 對應(yīng)的存取方法。在對一個(gè)類發(fā)送了 valueForKey 消息后,KVC 總得能找到對應(yīng)的實(shí)現(xiàn)吧。
常見的存取模式
返回屬性值的方法格式為 -<key> ,方法返回一個(gè)對象、常量或結(jié)構(gòu)體。-is<key> 用于 Boolean 屬性。BOOL 類型在這里是比較特殊的。
另外還有一點(diǎn)需要注意的就是對于非對象類型的屬性值,如果被設(shè)置為 nil,需要做特殊處理。子類化 setNilValueForKey 方法并做特殊判斷即可。
一對多關(guān)系(To-Many)中的集合訪問器方法
盡管仍然可以通過 -<key> 和 -set<Key>: 的方式處理對多關(guān)系的屬性,但是這樣并不是很高效,因?yàn)槟阍趫?zhí)行操作前需要將集合類型解包出來。所以最好的方式仍然是提供額外的存取器方法。
比如對于 Person 類來說,它的 friendNames 屬性是許多個(gè)人的名字,屬于集合類型,這是一個(gè)典型的"一對多"關(guān)系。對于它的訪問:
- 間接:通過 KVC 獲取到集合屬性,比如一個(gè) NSArray 的對象,然后對這個(gè)對象進(jìn)行操作
- 直接:實(shí)現(xiàn) Apple 提供的方法模型,以達(dá)到訪問的目的。
通過實(shí)現(xiàn)集合的存取方法,我們可以模擬出一個(gè)在類外面看起來是集合的對象。這樣我們通過在類的內(nèi)部實(shí)現(xiàn)相關(guān)的 KVC 集合方法,類的外面在調(diào)用時(shí),根本感覺不到類里面使用 KVC 實(shí)現(xiàn)的。
這些思想得用具體的代碼實(shí)現(xiàn)一下才能體會到 KVC 的特性。
這里有兩種差異較大的集合存取器。
有序的集合
有序的集合關(guān)系中,存在計(jì)總、取回、添加和替換等操作。通常這種關(guān)系是 NSArray 或 NSMutableArray 的實(shí)例。
Getter
為了支持只讀的訪問屬性:
- -countOf<Key>,必須,類似于 NSArray 的 count 方法。
- -objectIn<Key>AtIndex: 或 -<key>AtIndexes:必須實(shí)現(xiàn),相當(dāng)于 NSArray 類的 objectAtIndex: 和 objectsAtIndexes: 方法。
- -get<Key>:range:。可選,但是能獲得額外的收益。相當(dāng)于 NSArray 的 getObjects:range:。
Mutable Index Accessor
對于可變的版本,只需額外實(shí)現(xiàn)幾個(gè)方法即可。
- -insertObject:in<Key>AtIndex: or -insert<Key>:atIndexes:至少實(shí)現(xiàn)一個(gè)。
- -removeObjectFrom<Key>AtIndex: or -remove<Key>AtIndexes:也是至少實(shí)現(xiàn)一個(gè)。
- -replaceObjectIn<Key>AtIndex:withObject: or -replace<Key>AtIndexes:with<Key>: 可選的,實(shí)現(xiàn)了能提高性能。
可以看出,這些方法在 NSMutableArray 中都有對應(yīng)的實(shí)現(xiàn)。
無序的訪問器模式
無序的存取器方法給可變的對象提供了一套訪問機(jī)制。對象很可能是 NSSet 或 NSMutableSet 的實(shí)例。
Getter 需要實(shí)現(xiàn)的方法
- -countOf<Key>
- -enumeratorOf<Key>
- -memberOf<Key>:
Mutable 需要實(shí)現(xiàn)的方法
- -add<Key>Object: or -add<Key>:
- -remove<Key>Object: or -remove<Key>:
- -intersect<Key>:
Key Value 驗(yàn)證
KVC 為驗(yàn)證屬性值提供了一致的 API。驗(yàn)證機(jī)制提供了一個(gè)類,使得有機(jī)會接受一個(gè)值,提供一個(gè)替換值,或者否認(rèn)一個(gè)新值并給出錯(cuò)誤原因。
通過驗(yàn)證方法,當(dāng) setValueForKey 方法傳入一個(gè)新值時(shí),我們有機(jī)會對這個(gè)值進(jìn)行檢查,然后來做一些處理。這樣子對于值的驗(yàn)證集中在驗(yàn)證方法中,外界的業(yè)務(wù)邏輯處理變得很清楚。
舉個(gè)例子:
驗(yàn)證方法命名習(xí)慣
驗(yàn)證方法的命名格式為 validate<Key>:error:
// 假如當(dāng)前對象有屬性 name
- (BOOL)validateName:(id *)iovalue error:(NSError **)error {
// 方法實(shí)現(xiàn)
}
實(shí)現(xiàn)一個(gè)驗(yàn)證方法
上面的驗(yàn)證方法提供了兩個(gè)參數(shù)的引用:需要驗(yàn)證的值以及需要返回的錯(cuò)誤信息。
對于上述方法可能有三個(gè)結(jié)果:
- 驗(yàn)證成功,返回 YES 并且不改變 error 對象。
- 值無法通過驗(yàn)證并且不能根據(jù)其創(chuàng)建合法的值,這時(shí)需要返回 NO 并且附上具體的 NSError
- 能夠根據(jù)傳入的值創(chuàng)建正確的值,將其返回即可。
具體可以看下官網(wǎng)文檔中的示例:
-(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
// The name must not be nil, and must be at least two characters long.
if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
if (outError != NULL) {
NSString *errorString = NSLocalizedString(@"A Person's name must be at least two characters long",@"validation: Person, too short name error");
NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorString};
*outError = [[NSError alloc] initWithDomain:PERSON_ERROR_DOMAIN
code:PERSON_INVALID_NAME_CODE
userInfo:userInfoDict];
}
return NO;
}
return YES;
}
如果值無法通過驗(yàn)證時(shí),需要首先檢查 outError 參數(shù)是否為 nil,如果不是,需要將其設(shè)置為正確的值。
調(diào)用驗(yàn)證方法
可以直接調(diào)用該方法或者通過 validateValue:forKey:error: 指定 key 。將默認(rèn)的去查找并匹配該 key 。如果找到了對應(yīng)的方法,將按照其返回作為結(jié)果。如果未找到,將返回 YES 作為結(jié)果。
自動驗(yàn)證
一般來說,并不會自動調(diào)用驗(yàn)證方法,只有在使用 CoreData ,數(shù)據(jù)保存時(shí)會自動調(diào)用。
驗(yàn)證方法給我們提供了一種糾正錯(cuò)誤的機(jī)會,例如這里傳入待檢查的參數(shù)是人名字符串,我們可以在這里將空格過濾掉,然后返回沒有空格的名字。并且判斷是否含有非法字符串,如果有非法字符串,就直接返回 NO 表示驗(yàn)證不能通過。
驗(yàn)證常量
驗(yàn)證方法默認(rèn)參數(shù)是對象,對于常量和結(jié)構(gòu)體需要單獨(dú)做處理。