天天看點

Key-Value Coding

介紹

Key-Value Coding簡稱KVC,中文名為鍵值編碼。它是一種利用字元串間接通路對象屬性的方法。而這個字元串便就是鍵。通路器,即setter和getter,也是一種間接通路對象屬性的方法,隻不過在有些場合更加适合使用KVC。雖然KVC在業務邏輯中很少會使用,但在Key-Value Observing,Core Data, Cocoa bindings, scripatability這些模式或者進階特性的基礎,是以KVC很重要。

在如下場合,利用KVC明顯比利用通路器來得友善。假設一個要從model層擷取所有聯系人顯示到NSTableView上(Cocoa裡的清單)。最普通的方式如下:

- (id)tableView:(NSTableView *)tableview
      objectValueForTableColumn:(id)column row:(NSInteger)row {

    Person *person = [contact objectAtIndex:row];
    if ([[column identifier] isEqualToString:@"name"]) {
        return [person name];
    }
    if ([[column identifier] isEqualToString:@"age"]) {
        return [person age];
    }
    if ([[column identifier] isEqualToString:@"favoriteColor"]) {
        return [person favoriteColor];
    }
    // And so on.
}
           

仔細想想,如果一個person的屬性很多,那麼代碼會變得十分龐大,并且很難維護,如果利用KVC則會簡潔得多

- (id)tableView:(NSTableView *)tableview
      objectValueForTableColumn:(id)column row:(NSInteger)row {

    Person *person = [contact objectAtIndex:row];
    return [person valueForKey:[column identifier]];
}
           
PS: 這裡不了解KVC沒關系,可以看完後面的實作,再來體會這裡。

相關術語

在介紹如何實作KVC之前,先介紹幾個接下來要使用的術語:

  • attribute. 它是一個簡單屬性,不存在一對一關系,一對多關系,隻是描述某個事物的一個方面,不和其他對象産生任何聯系。例如:标量,字元串,布爾值,NSNumber以及其他不可變的對象如NSColor等。
  • to-one relationship. 它反映的一對一關系屬性,與之對應的對象和它獨立變化。例如:UIView的superView就是一個to-one relationship屬性。
  • to-many relationship. 它反映的是一對多屬性關系,通常用集合來表示這樣的關系。例如:NSArray, NSSet等。當然也可以利用後面介紹的方法,來自定義KVC的通路方式,在宏觀上反映出一對多屬性關系。
  • key. key就是一個唯一辨別對象屬性的字元串。通常就是屬性名,如person的name屬性,則key=”name”。當然可以自定義,它與具體KVC的通路方法的實作或者執行個體變量有關,後面會介紹。
  • key path. key path是利用“.”分割的一串字元串,用來辨別一系列對象屬性的層層穿越。例如一個國家有好多城市,一個城市有很多街道,則如果我擁有國家這個對象country,要通路到這個國家的中的某條街道,則key path=”country.city.street”

具體實作

擷取attribute簡單屬性的方法

[object valueForKey: @"keywords"];
           

object: 擁有屬性的對象。

keywords: 唯一辨別屬性的字元串key。

舉例,例如要通路一個人person的名字name屬性.

當然除了這樣,還需要通過指定對應的通路器,來将key和屬性産生關系。

-(void)setName: (NSString *)name
{
    self.name = name;
}

-(NSString *)name
{
    return self.name;
}
           

看起來和setter,getter幾乎是一模一樣的,沒錯,就是一模一樣。KVC的通路器,和關聯屬性的key有關。其内容在後面一節介紹。這裡你僅需知道,它不是一般意義上的setter,getter通路器。

如果沒有指定KVC通路器,那麼接收對象會給自己發一個

valueForUndefinedKey:

消息。而這個消息在

NSKeyValueCoding

中定義,其預設實作是抛出一個

NSUndefinedKeyException

異常。你可以通過重寫這個方法,來達到自定義需求。

相類似的,利用key path實作:

[object valueForKeyPath: @"parentKey.subKey"];
           

object: 擁有屬性的對象。

parentKey.subKey: 唯一辨別屬性路徑的字元串key path。

舉個例子,通路一個國家的某個城市的,某個街道

在key path的每個子路徑都需要實作與之對應的KVC通路器,如果其中任何一個沒有與之對應的通路器,則出出發

valueForUndefinedKey:

消息。

PS: 如果country的城市屬性是一個集合citys,而每個城市又有街道的集合streets,則通過”citys.streets”這個key path會傳回這個國家中所有城市的所有街道。

當然如果你想在一個語句中擷取多個屬性值,則利用

dictionaryWithValuesForKeys:

方法會傳回一個NSDictionary,裡面包含屬性的key和對應屬性的值。

PS: 在集合對象中,NSArray, NSSet, NSDictionary不能包含nil來表示空值,因為nil表示集合結束。如果要表示空值,請用NSNull,NSNull是一個單例類。[NSNull null]來通路單例對象。還有

dictionaryWithValuesForKeys:

setValuesForKeysWithDictionary:

提供了對NSNull和nil的自動拆箱和裝箱的橋接。

設定attribute簡單屬性的方法

[object setValue: valueObject forKey: @"key"];
           

舉個例子

[person setValue: @"xiaoming" forKey: @"name"];
           

同樣的道理,需要關聯KVC通路器,如果沒有關聯通路器,接收消息對象會給自己發送

setValue:forUndefinedKey:

消息,其預設實作是抛出

NSUndefinedKeyException

異常。

對key path的設定以及同時設定多個屬性的方式也是和通路差不多的

[object setValue: valueObject forKeyPath: @"parentKey.subKey"];
[object setValuesForKeysWithDictionary: @{"key1": value1, ....}];
           

點文法和KVC

點文法和KVC兩者很類似,隻不過原理不同。點文法依賴的是标準通路器setter,getter的實作,KVC依賴的自己的那套通路器實作。兩者可以一起使用,不會幹擾對方,如下所示:

// KVC
MyClass *myInstance = [[MyClass alloc] init];
NSString *string = [myInstance valueForKey:@"stringProperty"];
[myInstance setValue:@2 forKey:@"integerProperty"];

// 點文法
MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
myInstance.linkedInstance.integerProperty = ;
           

兩者效果是一樣的。

KVC通路器方法

前面多次提到,需要用KVC通路器關聯key和對應的屬性,這一節就是介紹這個重要的KVC通路器方法。其模式如下:

-set<Key>
-<Key>
           

這裡

<Key>

對應的

valueForKey:

的關鍵字,如果對象屬性name,不想定義成用@“name”檢索屬性值,而是用@“myName”去檢索那麼對應的KVC通路器方法應該設定成如下:

-(void)setMyName: (NSString *) name
{
    self.name = name;
}
-(NSString *)myName
{
    return self.name;
}
           

當然,如果屬性是個布爾值,它的KVC getter通常我們按照習慣會是

-is<Key>

的形式。這樣也是隻支援的

- (BOOL)isHidden {
   // Implementation specific code.
   return ...;
}
           

當遇到不支援nil的資料類型時,比如說BOOL,那麼還得實作接收對象的

setNilValueForKey:

方法:

- (void)setNilValueForKey:(NSString *)theKey {

    if ([theKey isEqualToString:@"hidden"]) {
        [self setValue:@YES forKey:@"hidden"];
    }
    else {
        [super setNilValueForKey:theKey];
    }
}
           

這裡相當于當給hidden,調用

setNilValueForKey: @"hidden"

的時候,hidden會被設定為YES。

當對于關系到集合類型屬性的時候,也就是to-many relationship的屬性。如果隻用

-set<Key>

-<Key>

去實作,那樣隻能對集合對象操作,而無法對其存儲的元素操作。如果需要對其存儲的元素操作,需要用到集合通路器代理

mutableArrayValueForKey:

或者

mutableSetValueForKey:

實作集合通路器或者叫做mutable通路器(mutable accessor),可以有很多好處:

  • 對一對多關系的高性能操作
  • 除了用NSArray和NSSet還可以實作合适的集合通路器,宏觀上提供自定義的集合類型
  • 在Key-Value Observing中可以利用集合通路器直接通知觀察者。

集合通路器有兩種類型:索引通路器(index accessor)和無序通路器(unordered accessor)。前者針對NSArray這樣的有序集合,而後者針對NSSet這樣的無序集合。

你可以通過索引通路器來獲得有序集合的内容資料,通過mutable通路器來提供一個修改集合屬性的接口。

getter索引通路器需要實作如下内容:

  • -countOf<Key>

  • -objectIn<Key>AtIndex:

    或者

    -<Key>AtIndexes:

  • get<Key>:range:

假設某個對象有個屬性是employees,其類型為NSArray。它的索引通路器需要如下實作代碼:

//-countOf<Key>
- (NSUInteger)countOfEmployees {
    return [self.employees count];
}

//-objectIn<Key>AtIndex:
- (id)objectInEmployeesAtIndex:(NSUInteger)index {
    return [employees objectAtIndex:index];
}

//-<Key>AtIndexes:
- (NSArray *)employeesAtIndexes:(NSIndexSet *)indexes {
    return [self.employees objectsAtIndexes:indexes];
}

//get<Key>:range:
- (void)getEmployees:(Employee * __unsafe_unretained *)buffer range:(NSRange)inRange {
    // Return the objects in the specified range in the provided buffer.
    // For example, if the employees were stored in an underlying NSArray
    [self.employees getObjects:buffer range:inRange];
}
           

mutable索引通路器需要實作如下内容

mutable索引通路器相當于對普通屬性的setter通路器。提供mutable索引通路器可以給你帶來更加高效的集合屬性内容的操作。

  • -insertObject:in<Key>AtIndex:

    或者

    -insert<Key>:atIndexes:

  • -removeObjectFrom<Key>AtIndex:

    或者

    -remove<Key>AtIndexes:

  • -replaceObjectIn<Key>AtIndex:withObject:

    或者

    -replace<Key>AtIndexes:with<Key>:

同樣假設對象擁有一個NSArray類型的employees屬性,其實作mutable索引通路器代碼如下:

// -insertObject:in<Key>AtIndex:
- (void)insertObject:(Employee *)employee inEmployeesAtIndex:(NSUInteger)index {
    [self.employees insertObject:employee atIndex:index];
    return;
}

// -insert<Key>:atIndexes:
- (void)insertEmployees:(NSArray *)employeeArray atIndexes:(NSIndexSet *)indexes {
    [self.employees insertObjects:employeeArray atIndexes:indexes];
    return;
}

// -removeObjectFrom<Key>AtIndex:
- (void)removeObjectFromEmployeesAtIndex:(NSUInteger)index {
    [self.employees removeObjectAtIndex:index];
}

// -remove<Key>AtIndexes:
- (void)removeEmployeesAtIndexes:(NSIndexSet *)indexes {
    [self.employees removeObjectsAtIndexes:indexes];
}

// -replaceObjectIn<Key>AtIndex:withObject:
- (void)replaceObjectInEmployeesAtIndex:(NSUInteger)index
                             withObject:(id)anObject {

    [self.employees replaceObjectAtIndex:index withObject:anObject];
}

// -replace<Key>AtIndexes:with<Key>:
- (void)replaceEmployeesAtIndexes:(NSIndexSet *)indexes
                    withEmployees:(NSArray *)employeeArray {

    [self.employees replaceObjectsAtIndexes:indexes withObjects:employeeArray];
}
           

上面講的都是對有序集合的操作,接下來分析對無序集合的通路

getter無序通路器其實作如下:

  • -countOf<Key>

  • -enumeratorOf<Key>

  • -memberOf<Key>:

假設對象擁有NSSet的transactions屬性:

// -countOf<Key>
- (NSUInteger)countOfTransactions {
    return [self.transactions count];
}

// -enumeratorOf<Key>
- (NSEnumerator *)enumeratorOfTransactions {
    return [self.transactions objectEnumerator];
}

// -memberOf<Key>:
- (Transaction *)memberOfTransactions:(Transaction *)anObject {
    return [self.transactions member:anObject];
}
           

mutable無序通路器實作如下方法:

  • -add<Key>Object:

    或者

    -add<Key>:

  • -remove<Key>Object:

    或者

    -remove<Key>:

  • -intersect<Key>:

具體實作如下:

// -add<Key>Object:
- (void)addTransactionsObject:(Transaction *)anObject {
    [self.transactions addObject:anObject];
}

// -add<Key>:
- (void)addTransactions:(NSSet *)manyObjects {
    [self.transactions unionSet:manyObjects];
}

// -remove<Key>Object:
- (void)removeTransactionsObject:(Transaction *)anObject {
    [self.transactions removeObject:anObject];
}

// -remove<Key>:
- (void)removeTransactions:(NSSet *)manyObjects {
    [self.transactions minusSet:manyObjects];
}

// -intersect<Key>:
- (void)intersectTransactions:(NSSet *)otherObjects {
    return [self.transactions intersectSet:otherObjects];
}
           

鍵值驗證

鍵值驗證就是在設定值之前的一種“防禦”措施。提供在設定之前自定義驗證其請求合理性,其形式為

validate<Key>:error:

, 如下代碼是對屬性name的驗證代碼:

-(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {
    // Implementation specific code.
    return ...;
}
           

其中

validate<Key>:error:

有兩個參數,第一個參數則為使用者請求設定的值。第二個參數則是錯誤指針,包含錯誤資訊。其具體實作可參考如下步驟:

  • 如果請求設定值是合理的,則傳回YES不報錯。
  • 如果請求設定值不合理,則傳回NO,并将相關錯誤資訊通過錯誤指針帶出。
  • 如果一個新的合理對象被建立,并且指派給對應屬性,除了傳回YES之外,還要傳回對應的對象。錯誤不是必須傳回的。

驗證執行個體代碼如下:

-(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] < )) {
        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;
}
           
PS: 如果驗證方法傳回NO,那麼必須檢查outError是不是NULL,如果NULL則必須給予合理的NSError對象指派。

如何調用驗證方法?第一種是直接調用驗證方法,第二種則是通過

validateValue:forKey:error:

,它會自動尋找到對應的

validate<Key>:error:

方法去執行驗證。如果不能找到對應的方法,則會傳回YES,并且驗證值。

PS: 在

-set<Key>

絕對不要調用驗證方法,否則會進入死循環。

如何自動驗證?其實就KVC是不支援自動驗證的,因為那是業務邏輯的責任,如果非要支援自動驗證,應該借助于Cocoa binding這個技術。另外還有Core Data下當managedObject上下文被儲存的時候,也會自動驗證。

KVC Compliance

對于attribute和to-one relatioship按照如下方式檢查是否遵守KVC:

  • 實作方法名字中帶有

    -<Key>

    ,

    -is<Key>

    或者擁有執行個體變量

    <key>

    _<key>

    。雖然習慣上遵守小寫字母開頭,但是KVC也是支援大小字母開頭的例如URL。
  • 如果屬性可更改,那麼必須實作

    -set<Key>

  • 如果類需要支援設定值前的驗證,那麼必須實作

    -validate<Key>:error:

  • -set<Key>

    一定不能調用驗證代碼

對于有序可索引的to-many relationship(e.g. NSArray)的實作如下:

  • 實作

    -<Key>

    或者擁有執行個體标量

    <key>

    _<key>

  • 或者實作

    -countOf<Key>

    以及至少實作

    -objectIn<Key>AtIndex:

    -<key>AtIndexes:

    中的一個
  • 當然利用

    -get<Key>:range:

    在适當的地方來提高性能

如果有序可索引的to-many relationship還需支援更改内容,還需要實作如下方法:

  • -insertObject:in<Key>AtIndex:

    -insert<Key>:atIndexes:.

  • -removeObjectFrom<Key>AtIndex:

    -remove<Key>AtIndexes:

  • 如果要替換對象,你還可以選擇性的實作

    -replaceObjectIn<Key>AtIndex:withObject:

    或-

    replace<Key>AtIndexes:with<Key>:

對于無序不可索引的to-many relationship(e.g. NSSet)的實作如下:

  • 實作

    -<key>

    或者擁有執行個體标量

    <key>

    _<key>

  • 或者實作

    -countOf<Key>

    ,

    -enumeratorOf<Key>

    ,

    -memberOf<Key>:

對于無序不可索引的to-many relationship還需支援更改内容,還需實作如下方法:

  • 至少實作

    -add<Key>Object:

    -add<Key>:

    中的一個
  • 至少實作

    -remove<Key>Object:

    ,

    -remove<Key>

    中的一個
  • 可選實作

    -intersect<Key>:

    -set<Key>:

對于标量和結構體的支援

由于KVC是基于對象的一種機制,

valueForKey:

setValue:forKey:

遇到标量和結構體時會自動封裝成對象。而對于nil值利用前面提過的

setNilValueForKey:

的重寫來實作處理成有意義的值。而結構體和标量對應對象的關系表如下:

Key-Value Coding
Key-Value Coding
Key-Value Coding

集合操作符

對于一些集合類型的屬性,可以對比資料庫中的聚合函數,對集合裡的内容進行操作,其表達式定義如下:

Key-Value Coding

在正常的key path的要操作集合前加

@操作内容

,例如統計數量

@count

,求和

@sum

舉一些例子:

// 求平均值
NSNumber *transactionAverage = [transactions valueForKeyPath:@"@avg.amount"];
// 統計數量
NSNumber *numberOfTransactions = [transactions valueForKeyPath:@"@count"];
// 求最大值
NSDate *latestDate = [transactions valueForKeyPath:@"@max.date"];
// 求最小值
NSDate *earliestDate = [transactions valueForKeyPath:@"@min.date"];
// 求和
NSNumber *amountSum = [transactions valueForKeyPath:@"@sum.amount"];
           

除了聚合函數之外,還有支援關系運算的對象操作符号,其使用方法和前者一樣,隻不過含義不同,舉一些例子:

// 去重合并所有支付賬單
NSArray *payees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
// 非去重合并支付賬單
NSArray *payees = [transactions valueForKeyPath:@"@unionOfObjects.payee"];
           

還有數組以及集合操作符

// 傳回所有交易事務中去重支付賬單
NSArray *payees = [arrayOfTransactionsArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];

// 傳回所有交易事務中非去重支付賬單
NSArray *payees = [arrayOfTransactionsArrays
valueForKeyPath:@"@unionOfArrays.payee"];

// 對于集合NSSet操作類型不一一列舉了。
           

通路器搜尋實作細節

setValue:forKey:

的通路器搜尋順序預設實作方式如下:

  1. 先搜尋接收者是否存在

    -set<Key>

    通路器,如果存在則調用。
  2. 如果沒有通路器存在,且接收者類方法

    accessInstanceVariablesDirectly

    傳回YES。那麼按順序搜尋符合模式

    _<key>

    ,

    _is<Key>

    ,

    <key>

    ,

    is<Key>

    的執行個體變量
  3. 如果沒有通路器和執行個體變量,則從Non-Object Values中提取值。
  4. 如果以上方法都沒辦法執行,則

    setValue:forUndefinedKey:

    被執行。

valueForKey:

的通路器搜尋順序預設實作方式如下:

  1. 先搜尋接收者是否存在

    -get<Key>

    ,

    -<key>

    ,

    -is<Key>

    如果能搜尋到,則執行。傳回值一定是對象,如果遇到标量,結構體則會進行自動裝箱。
  2. 如果不存在1的情況則繼續搜尋

    -countOf<Key>

    ,

    objectIn<Key>AtIndex:

    ,

    <key>AtIndexes:

    ,後兩者至少實作一個。
  3. 如果不存在2的情況則繼續搜尋

    -countOf<Key>

    ,

    -enumeratorOf<Key>

    ,

    memberOf<Key>:

  4. 如果不存在3的情況,且

    accessInstanceVariablesDirectly

    傳回YES,則按照順序搜尋

    _<key>, _is<Key>

    ,

    <key>

    ,

    is<Key>

    模式的執行個體變量
  5. 如果以上情況都不符合,則

    valueForUndefinedKey:

    會被調用。

* 更多關于搜尋内容(如有序集合,無序集合等内容)參考蘋果官方開發文檔Key-Value Coding *

繼續閱讀