iOS KVC小結(jié)
KVC的概念
KVC,即Key-value coding,鍵值編碼。給我們提供了一套更加直接的方式,來訪問一個(gè)對象的屬性,或者給對象的屬性賦值。而不是通過setter和getter方法。是一種可以直接通過名字或者key來獲取對象的屬性的機(jī)制。KVC的功能很強(qiáng)大,也屬于iOS的黑魔法之一。類的只讀屬性或者私有屬性(iOS 13以后,一些控件的私有屬性就不能獲取了,運(yùn)行時(shí)會報(bào)錯(cuò);但是自定義的類,用代碼測試還可以使用),只要能獲取到屬性的名字,使用KVC都可以進(jìn)行值的操作。
成員變量、屬性
開始之前,先來回憶一下成員變量、屬性的概念,以便于對后面的KVC的流程有更清楚的理解。
@interface KVCPerson : NSObject
{
//成員變量
NSString * _name;
}
//屬性
@property (nonatomic, copy) NSString * address;
@end
創(chuàng)建了一個(gè)KVCPerson類,代表一個(gè)人。一個(gè)人有名字、家庭住址。所以我們把名字添加為“成員變量”,把家庭住址添加為“屬性”。
-
在大括號
{}中聲明的變量都是成員變量。成員變量不會自動(dòng)生成setter、getter方法。- 成員變量的取值/賦值,使用
->。例如:p->name
- 成員變量的取值/賦值,使用
-
OC中用
@property來聲明一個(gè)屬性,編譯器會自動(dòng)為屬性生成“成員變量”,并且為成員變量生成對應(yīng)的setter和getter方法。屬性可以使用“點(diǎn)語法”進(jìn)行操作。例如:
p.name獲取KVCPerson類的IvarList和MethodList展示如下:
//成員變量列表
mIvarList ==
(
"_name",
"_address"
)
//方法列表
mMthodList ==
(
getIvarList,
getMethodList,
//自動(dòng)生成的address的getter方法
address,
//自動(dòng)生成的address的setter方法
"setAddress:",
init,
".cxx_destruct"
)
KVC常用的API
設(shè)置值的方法
//給定一個(gè)值和一個(gè)屬性的鍵名,設(shè)置該屬性的值。
- (void)setValue:(nullable id)value forKey:(NSString *)key;
//給定一個(gè)值和一個(gè)“屬性的屬性”的鍵名,設(shè)置該“屬性的屬性”的值。like:address.city。
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
//處理設(shè)置值的時(shí)候沒有找到鍵名的異常情況的API。找不到對應(yīng) key 命名的屬性時(shí),就會 NSUnknownKeyException 異常崩潰,可以在對象里重寫下面兩個(gè)方法,防止崩潰。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//如果將非對象類型的屬性設(shè)置為 nil的情況下,會報(bào)NSInvalidArgumentException 的異常;可以使用這個(gè)方法來處理異常,防止崩潰
- (void)setNilValueForKey:(NSString *)key;
在給屬性賦值的時(shí)候,KVC不會像setter、getter方法那么溫柔,對于類的只讀屬性或者私有屬性,setter和getter是訪問不到的。
相比之下,KVC就會顯得比較粗暴。一個(gè)類的只讀屬性、私有屬性,都可以進(jìn)行賦值操作。
獲取值的方法
//給定一個(gè)鍵名,獲取到該鍵對應(yīng)的值
- (nullable id)valueForKey:(NSString *)key;
//給一個(gè)“屬性的屬性”的鍵名,獲取到該“屬性的屬性”的值。這個(gè)方法結(jié)合操作符做到一些比較好玩的事情
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//獲取值操作的情況下,沒有找到鍵名,會拋出異常。在類中可以重寫這個(gè)方法,防止程序的崩潰。
- (nullable id)valueForUndefinedKey:(NSString *)key;
沒有setter/getter等方法情況下,根據(jù)這個(gè)方法的返回決定是否直接查詢成員變量
//在找不到屬性的setter/getter方法的情況下,是否直接去查詢屬性的成員變量。默認(rèn)返回yes
+ (BOOL)accessInstanceVariablesDirectly;
屬性值正確性驗(yàn)證
//屬性的正確性驗(yàn)證,默認(rèn)返回yes??梢詫σ粋€(gè)屬性進(jìn)行驗(yàn)證,如果符合邏輯條件就可以返回yes,否則返回false。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
使用案例:
驗(yàn)證年齡的合理性,如果年齡超過200歲就不合情理了,所以可以對年齡加上正確性驗(yàn)證。在KVCPerson類中增加一個(gè)屬性age,并添加驗(yàn)證代碼。
//正確性驗(yàn)證
- (BOOL) validateAge:(inout id _Nullable __autoreleasing *)ioValue error:(out NSError * _Nullable __autoreleasing *)outError{
NSNumber* value = (NSNumber*)*ioValue;
if ([value integerValue] >= 0 && [value integerValue] <= 200) {
return YES;
}
return NO;
}
在使用KVCPerson類的時(shí)候,添加年齡的驗(yàn)證邏輯代碼
KVCPerson * p = [[KVCPerson alloc] init];
NSNumber * value = @200;
if ([p validateValue:&value forKey:@"age" error:NULL]) {
NSLog(@"我的數(shù)據(jù)不正確");
}
KVC賦值的流程順序
在調(diào)用- (void)setValue:(nullable id)value forKey:(NSString *)key方法的時(shí)候,執(zhí)行的流程如下:
-
查詢類中的有沒有相關(guān)的方法實(shí)現(xiàn):
查詢類中有沒有
set<key>:(NSString *)key的方法。有的話通過setter方法給屬性設(shè)值;否則繼續(xù)查找。查詢類中有沒有
_set<key>:(NSString *)key的方法。有的話通過setter方法給屬性設(shè)值;否則繼續(xù)查找。查詢類中有沒有
setIs<key>:(NSString *)key的方法。有的話通過setter方法給屬性設(shè)值;沒有的話,需要去查詢accessInstanceVariablesDirectly的返回值。
-
沒有查詢到上述的方法的情況下,會去查詢
accessInstanceVariablesDirectly這個(gè)方法的返回值(沒有重寫的話,默認(rèn)返回YES)。如果該方法的返回值是NO的情況下,會拋出
NSUnknownKeyException異常如果該方法的返回值是YES的情況下,會去找類中的成員變量,并且按照一定的優(yōu)先級順序先匹配成員變量。獲取到匹配的成員變量后,直接賦值。
-
沒有找到對應(yīng)的
setter方法,直接給成員變量賦值的流程按照優(yōu)先級順序進(jìn)行匹配成員變量:
_<key>>_is<Key>><key>>is<Key>按照順序查詢到一個(gè)能匹配的成員變量后,就給該成員變量賦值。
假設(shè)以上四種形式的成員變量都存在,只匹配優(yōu)先級最高的一個(gè)。
流程圖顯示如下:

KVC取值的流程順序
調(diào)用- (nullable id)valueForKey:(NSString *)key方法的時(shí)候,執(zhí)行的流程如下:
-
查詢類的方法列表中有沒有相關(guān)的
getter方法實(shí)現(xiàn)查詢類中有沒有
- (NSString *)get<Key>方法。有的話通過getter方法取出屬性的值;否則繼續(xù)查找。查詢類中有沒有
- (NSString *)<key>方法。有的話通過getter方法取出屬性的值;否則繼續(xù)查找。查詢類中有沒有
- (NSString *)is<Key>方法。有的話通過getter方法取出屬性的值;否則繼續(xù)查找。查詢類中有沒有
- (NSString *)_<key>方法。有的話通過getter方法取出屬性的值;沒有的話,需要去查詢accessInstanceVariablesDirectly的返回值。
-
沒有查詢到上述的方法的情況下,會去查詢
accessInstanceVariablesDirectly這個(gè)方法的返回值(沒有重寫的話,默認(rèn)返回YES)如果該方法的返回值是NO的情況下,會拋出
NSUnknownKeyException異常如果該方法的返回值是YES的情況下,會去找類中的成員變量,并且按照一定的優(yōu)先級順序先匹配成員變量。獲取到匹配的成員變量后,取出該成員變量的值。
-
沒有找到對應(yīng)的
getter方法,就需要直接查詢匹配的成員變量,然后取出該成員變量的值。按照優(yōu)先級順序進(jìn)行匹配成員變量:
_<key>>_is<Key>><key>>is<Key>按照順序查詢到一個(gè)能匹配的成員變量后,就取出該成員變量的值。
假設(shè)以上四種形式的成員變量都存在,只匹配優(yōu)先級最高的一個(gè)。
流程圖顯示如下:

KVC集合操作符
在學(xué)習(xí)KVC的時(shí)候,看到了KVC集合操作符,眼前一亮啊。所以,為了以后能夠隨時(shí)看一下,在這里進(jìn)行記錄。
KVC集合操作符允許在valueForKeyPath:方法中使用操作運(yùn)算,作用于集合中所有的元素,來獲取到想要的成果。
集合操作符根據(jù)其返回值的不同,分為三個(gè)類型:
- 簡單的集合操作符,返回的是 strings, number, 或者 dates
* @count: 返回的值為集合中對象總數(shù),是`NSNumber`類型數(shù)據(jù)。
* @sum : 首先把集合中的每個(gè)對象都轉(zhuǎn)換為`double`類型,然后計(jì)算其總和,最后返回值為這個(gè)總和的`NSNumber`對象。
* @avg : 把集合中的每個(gè)對象都轉(zhuǎn)換為`double`類型,然后計(jì)算其平均值,返回值為平均值的`NSNumber`對象。
* @max : 使用`compare:`方法來確定最大值。所以為了讓其正常工作,集合中所有的對象都必須支持和另一個(gè)對象的比較。
* @min : 和`@max`一樣,但是返回的是集合中的最小值。
- 對象操作符,返回的是一個(gè)數(shù)組
@unionOfObjects / @distinctUnionOfObjects: 返回一個(gè)由操作符右邊的 key path 所指定的對象屬性組成的數(shù)組。兩個(gè)方法中@distinctUnionOfObjects會對數(shù)組去重, 而且@unionOfObjects不會.</pre>
- 數(shù)組和集合操作符, 返回的是一個(gè)數(shù)組或者集合
@distinctUnionOfArrays / @unionOfArrays: 返回了一個(gè)數(shù)組,其中包含這個(gè)集合中每個(gè)數(shù)組對于這個(gè)操作符右面指定的 key path 進(jìn)行操作之后的值。正如你期望的,distinct版本會移除重復(fù)的值。
@distinctUnionOfSets: 和@distinctUnionOfArrays差不多, 但是它期望的是一個(gè)包含著NSSet對象的NSSet,并且會返回一個(gè)NSSet對象。因?yàn)榧喜荒馨貜?fù)的值,所以它只有distinct操作。</pre>
事例數(shù)據(jù):
| name | price | date |
|---|---|---|
| iPhone 5 | 199 | September 21, 2012 |
| iPad Mini | 329 | November 2, 2012 |
| MacBook Pro | 1699 | June 11, 2012 |
| iMac | 1299 | November 2, 2012 |
創(chuàng)建一個(gè)類,類名為Product,如下。把以上數(shù)據(jù)包裝成對象,保存到productArray數(shù)組中。
@interface Product : NSObject
//產(chǎn)品名稱
@property (nonatomic, copy) NSString * name;
//產(chǎn)品價(jià)格
@property (nonatomic, assign) int price;
//產(chǎn)品的上市時(shí)間
@property (nonatomic, strong) NSDate * date;
@end
- 簡單的集合操作符,應(yīng)用實(shí)例:
NSString * count = [self.productArray valueForKeyPath:@"@count"];
NSString * sum_price = [self.productArray valueForKeyPath:@"@sum.price"];
NSString * avg_pric = [self.productArray valueForKeyPath:@"@avg.price"];
NSString * max_price = [self.productArray valueForKeyPath:@"@max.price"];
NSString * min_date = [self.productArray valueForKeyPath:@"@min.date"];
NSLog(@"count is %@",count); -----> count is 4
NSLog(@"sum_price is %@",sum_price); -----> sum_price is 3526
NSLog(@"avg_pric is %@",avg_pric); -----> avg_pric is 881.5
NSLog(@"max_price is %@",max_price); -----> max_price is 1699
NSLog(@"min_date is %@",min_date); -----> min_date is June 11, 2012
- 對象操作符,應(yīng)用實(shí)例(在
productArray數(shù)組中,使用第一條數(shù)據(jù)重復(fù)生成多次對象,能看到兩個(gè)方法的區(qū)別):
//@unionOfObjects方法,不會對數(shù)組去重
NSArray *unionOfObjects = [self.productArray valueForKeyPath:@"@unionOfObjects.name"];
//@distinctUnionOfObjects 會對數(shù)組去重
NSArray *distinctUnionOfObjects = [self.productArray valueForKeyPath:@"@distinctUnionOfObjects.name"];
//打印數(shù)據(jù)
NSLog(@"unionOfObjects is %@",unionOfObjects);
NSLog(@"distinctUnionOfObjects is %@",distinctUnionOfObjects);
-----------------------------------------------------------------------------------------------------
unionOfObjects is (
"iPhone 5",
"iPhone 5",
"iPhone 5",
"iPad Mini",
"MacBook Pro",
iMac
)
distinctUnionOfObjects is (
iMac,
"iPad Mini",
"MacBook Pro",
"iPhone 5"
)
- 數(shù)組和集合操作符, 應(yīng)用實(shí)例。(再復(fù)制一份商品數(shù)據(jù),稍加修改后,包裝成對象存放在一個(gè)數(shù)組中。并與存放上份數(shù)據(jù)的數(shù)組放在同一個(gè)數(shù)組中。)。集合運(yùn)算符與數(shù)組操作法是相似的,此處不做贅述。
//把兩份產(chǎn)品數(shù)據(jù)存放到同一個(gè)數(shù)組中
NSArray *arraysInArray = @[self.productArray, newProductArray];
//@unionOfArrays方法,不會對數(shù)據(jù)去重
NSArray *unionOfArrays = [arraysInArray valueForKeyPath:@"@unionOfArrays.name"];
//@distinctUnionOfArrays方法,會對獲取到的數(shù)據(jù)去重
NSArray *distinctUnionOfArrays = [arraysInArray valueForKeyPath:@"@distinctUnionOfArrays.name"];
//打印數(shù)據(jù)
NSLog(@"unionOfArrays is %@",unionOfArrays);
NSLog(@"distinctUnionOfArrays is %@",distinctUnionOfArrays);
-----------------------------------------------------------------------------------------------
unionOfArrays is (
"iPhone 5",
"iPhone 5",
"iPhone 5",
"iPad Mini",
"MacBook Pro",
iMac,
"iPhone 5 - 1",
"iPhone 5 - 1",
"iPhone 5 - 1",
"iPad Mini - 1",
"MacBook Pro - 1",
"iMac - 1"
),
distinctUnionOfArrays is (
"iPhone 5 - 1",
"iPad Mini - 1",
"iPhone 5",
"iPad Mini",
"iMac - 1",
"MacBook Pro - 1",
"MacBook Pro",
iMac
)