KVC(Key Value Coding)- Part 1

什么是 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é)果:

  1. 驗(yàn)證成功,返回 YES 并且不改變 error 對象。
  2. 值無法通過驗(yàn)證并且不能根據(jù)其創(chuàng)建合法的值,這時(shí)需要返回 NO 并且附上具體的 NSError
  3. 能夠根據(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ú)做處理。

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

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

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