介紹
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:
提供了對NSNull和nil的自動拆箱和裝箱的橋接。
setValuesForKeysWithDictionary:
設定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>
。雖然習慣上遵守小寫字母開頭,但是KVC也是支援大小字母開頭的例如URL。_<key>
- 如果屬性可更改,那麼必須實作
-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 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:
的通路器搜尋順序預設實作方式如下:
- 先搜尋接收者是否存在
通路器,如果存在則調用。-set<Key>
- 如果沒有通路器存在,且接收者類方法
傳回YES。那麼按順序搜尋符合模式accessInstanceVariablesDirectly
,_<key>
,_is<Key>
,<key>
的執行個體變量is<Key>
- 如果沒有通路器和執行個體變量,則從Non-Object Values中提取值。
- 如果以上方法都沒辦法執行,則
被執行。setValue:forUndefinedKey:
valueForKey:
的通路器搜尋順序預設實作方式如下:
- 先搜尋接收者是否存在
,-get<Key>
,-<key>
如果能搜尋到,則執行。傳回值一定是對象,如果遇到标量,結構體則會進行自動裝箱。-is<Key>
- 如果不存在1的情況則繼續搜尋
,-countOf<Key>
,objectIn<Key>AtIndex:
,後兩者至少實作一個。<key>AtIndexes:
- 如果不存在2的情況則繼續搜尋
,-countOf<Key>
,-enumeratorOf<Key>
memberOf<Key>:
- 如果不存在3的情況,且
傳回YES,則按照順序搜尋accessInstanceVariablesDirectly
,_<key>, _is<Key>
,<key>
模式的執行個體變量is<Key>
- 如果以上情況都不符合,則
會被調用。valueForUndefinedKey:
* 更多關于搜尋内容(如有序集合,無序集合等内容)參考蘋果官方開發文檔Key-Value Coding *