一、KVC簡介
KVC提供了一套不通過訪問器方法或者屬性變量,通過Key或者KeyPath直接訪問對象屬性的機制。KVC是以下技術的實現基礎KVO、Core Data、Cocoa bindings、AppleScript。KVC性能略遜于訪問器和實例變量,但是靈活性高,很多時候可以簡化代碼。使用KVC需要實現其存取方法,相關的方法都在Objective-C的NSKeyValueCoding協議中聲明,超級父類NSObject
默認遵守該協議。KVC支持對象屬性(如NSSting)同時也指出非對象屬性(基本數據類型和結構體,提供自動轉換數據類型)。
二、KVC基本原理
首先區分兩個基本概念
名稱 | 內容 |
---|---|
Key | Key是標識對象具體屬性的字符串,相當于對象的訪問器名稱或者變量名稱,不能包含空格。 |
KeyPath | KeyPath是指定對象一系列屬性,且用.分割每個屬性的字符串。字符串序列中的每個key標識前面對象的屬性。比如說people.address.street能夠獲取people的address屬性,然后獲取到address的street屬性。 |
然后說明等的執行過程,KVC的方法從功能上分存、取兩種方法setValue:forKey:
和valueForKey:
,以這兩個方法為代表描述執行過程。
首先setValue:forKey:
的執行過程
1、首先對象方法列表中匹配方法-set<Key>:
2、如果第1步失敗而且 accessInstanceVariablesDirectly
返回YES,按照以下順序匹配實例變量_<key>, _is<Key>, <key>, or is<Key>
3、如果前2步任一成功,則進行賦值。必要的話進行數據類型轉換。
4、如果前3步進行失敗則調用 setValue:forUndefinedKey:
拋出NSUndefinedKeyException
異常。
注:方法setValue:forKey:根據指定路徑獲取屬性值,KeyPath中每一個key都進行以上步驟;也就是說任何一個key出錯,都會拋出異常。
代碼2.1
@interface ViewController ()
{
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@property (nonatomic,copy)NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setValue:@"zwq" forKey:@"name"];
NSLog(@"_name:%@",_name);
NSLog(@"_isName:%@",_isName);
NSLog(@"name:%@",name);
NSLog(@"isName:%@",isName);
}
//可以通過以上代碼(注釋部分代碼)來驗證上述過程。
然后是valueForKey:
執行過程
1、首先按照此順序匹配方法 get<Key>, <key>, or is<Key>,
如果匹配成功調用方法,返回結果。必要的話進行數據類型轉換。
2、如果1步進行失敗,則匹配以下方法 countOf<Key>、 objectIn<Key>AtIndex: 、 <key>AtIndexes:
若找打其中一個,則返回容器類對象。該對象調用以上方法,會調用valueForKey:方法。(NSArray類的方法)
3、如果前2步失敗,則匹配以下方法countOf<Key>, enumeratorOf<Key>, and memberOf<Key>:
若找打其中一個,則返回容器類對象。該對象調用以上方法,會調用valueForKey:方法。
(NSSet類的方法)
4、如果前3步失敗,而且 accessInstanceVariablesDirectly
返回YES,按照以下順序匹配實例變量_<key>, _is<Key>, <key>, or is<Key>
。如果實例變量找到了,則進行復制。必要的話進行數據類型轉換。
5、如果前4步進行失敗則調用 valueForUndefinedKey:
拋出NSUndefinedKeyException
異常。
注:
1、方法valueForKeyPath:根據指定路徑獲取屬性值,KeyPath中每一個key都進行以上步驟;也就是說任何一個key出錯,都會拋出異常。
2、如果KeyPath序列中包含了一個key是一對多的關系,而且這個key不是最后一個,那么將返回所有對象的屬性值。例如accounts.transactions.payee將返回所有account的所有transaction的所有payee值。
//VC有一個數組屬性
@property (nonatomic,assign)NSArray *array;
- (void)viewDidLoad {
[super viewDidLoad];
//Data有一個name屬性
Data *data1 = [[Data alloc] init];
Data *data2 = [[Data alloc] init];
Data *data3 = [[Data alloc] init];
data1.name=@"data1";
data2.name=@"data2";
data3.name=@"data3";
//self.array.name
NSArray *arr = [NSArray arrayWithObjects:data1,data2,data3, nil];
[self setValue:arr forKey:@"array"];
NSLog(@"array:%@",[self valueForKeyPath:@"array.name"]);
}
輸出結果
2016-09-01 17:05:57.235 KVC[3467:249694] array:(
data1,
data2,
data3
)
可以仿照代碼2.1進行代碼驗證。由上邊底層執行過程不難看出:KVC性能略遜于訪問器和實例變量,但是靈活性高,視情況選擇。
說明:
1、必要的話進行數據類型轉換:KVC對應非對象類型進行自動數據類型轉換,下文做詳細說明。
2、方法accessInstanceVariablesDirectly的說明:默認返回YES,表示對象的實例變量可以直接訪問。
3、關于NSUndefinedKeyException異常的處理,下文做詳細說明
三、異常處理
1、方法valueForKey:
尋找不到指定Key或者KeyPath匹配的方法或變量名稱會自動調用valueForUndefinedKey:
拋出NSUndefinedKeyException
異常
2、方法setValue:forKey:
尋找不到指定Key或者KeyPath匹配的方法或變量名稱會自動調用setValue:forUndefinedKey:
拋出NSUndefinedKeyException
異常
//NSUndefinedKeyException如下所示
*** Terminating app due to uncaught exception 'NSUnknownKeyException',
reason: '[<ViewController 0x7fd60b728690> setValue:forUndefinedKey:]:
this class is not key value coding-compliant for the key age.'
處理方法為重寫此二者方法
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
方法體可為空也可自定義處理
//空處理
- (nullable id)valueForUndefinedKey:(NSString *)key
{
return nil;
}
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key
{
}
//自定義處理
- (nullable id)valueForUndefinedKey:(NSString *)key
{
if ([key isEqualToString:@"key"]) {
//返回內容自定義
return nil;
}
return nil;
}
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key
{
if ([key isEqualToString:@"key"])
{
//返回內容自定義
}
}
四、非對象類型的處理
KVC對于基本數據類型和結構體在底層支持自動數據類型轉換。根據相對的存取方法或者實例變量判端實際需要的值類型,選擇NSNumber 或 NSValue 進行自動轉換。
1、NSNumber對應的基本數據類型
例如
@property (nonatomic,assign)BOOL fail;
- (void)viewDidLoad {
[super viewDidLoad];
NSNumber *num = [NSNumber numberWithBool:0];
NSLog(@"class:%@",[num class]);
[self setValue:@"0" forKey:@"fail"];
NSLog(@"fali:%d--class:%@",self.fail,[[self valueForKey:@"fail"] class]);
}
輸出結果:
2016-09-01 14:27:33.401 KVC[2672:154097] class:__NSCFBoolean
2016-09-01 14:27:33.401 KVC[2672:154097] fali:0--class:__NSCFBoolean
2、NSValue對應的結構體類型
例如
@property (nonatomic,assign)CGPoint point;
NSValue *value = [NSValue valueWithCGPoint:CGPointMake(1, 1)];
NSLog(@"class:%@",[value class]);
[self setValue:value forKey:@"point"];
NSLog(@"fali:%@--class:%@",NSStringFromCGPoint(self.point) ,[[self valueForKey:@"point"] class]);
輸出結果:
2016-09-01 14:40:23.599 KVC[2751:163036] class:NSConcreteValue
2016-09-01 14:40:23.599 KVC[2751:163036] fali:{1, 1}--class:NSConcreteValue
3、注意事項
對非對象類型的屬性設置nil空值,底層調用setNilValueForKey:
,然后拋出NSInvalidArgumentException
異常
例如
[self setValue:nil forKey:@"fail"];
//或
[self setValue:nil forKey:@"point"];
異常:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '[<ViewController 0x7fd769484b90> setNilValueForKey]:
could not set nil as the value for the key fail.'
解決方法是重寫該方法setNilValueForKey:
,方法可空也可自定義處理,例如
-(void)setNilValueForKey:(NSString *)key
{
//自定義內容
if ([key isEqualToString:@"fail"])
{
[self setValue:[NSNumber numberWithBool:0] forKey:@"fail"];
}
if ([key isEqualToString:@"point"])
{
[self setValue:[NSValue valueWithCGPoint:CGPointZero] forKey:@"point"];
}
}
五、Key-Value Validation
這個標題就不翻譯了,英文更容易理解。
- validateValue:forKey:error:
- validateValue:forKeyPath:error:
KVC提供一套API使得屬性值生效。使得對象有機會接受值、提供默認值、拒絕新值、拋出錯誤原因。KVC不會自動調用,需要手動調用。默認實現過程:
1、調用validateValue:forKey:error:
2、在對象的方法列表中匹配validate<Key>:error:
3、如果找到則執行并返回結果
4、如果未找到則返回YES,并賦值
注意:set方法中禁止調用
@property (nonatomic,assign)NSInteger age;
-(BOOL)validateAge:(id *)ioValue error:(NSError **)outError
{
if (*ioValue == nil)
{
// 年齡大于0歲
[self setValue:@"0" forKey:@"age"];
return YES;
}
if ([*ioValue floatValue] <= 0.0)
{
if (outError != NULL)
{
NSString *errorString = NSLocalizedStringFromTable(
@"年齡要大于0歲", @"人",
@"年齡錯誤");
NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorString };
NSError *error = [[NSError alloc] initWithDomain:@"年齡校驗"
code:0
userInfo:userInfoDict];
*outError = error;
}
return NO;
}
else
{
return YES;
}
}
- (void)viewDidLoad {
[super viewDidLoad];
NSNumber *ageNum = [NSNumber numberWithInteger:0];
NSError *error = nil;
[self validateValue:&ageNum forKey:@"age" error:&error];
NSLog(@"error:%@",error);
}
輸出結果
2016-09-01 15:30:29.661 KVC[3044:197432] error:Error Domain=年齡校驗 Code=0 "年齡要大于0歲" UserInfo={NSLocalizedDescription=年齡要大于0歲}
五、容器類
關于KVC在容器類中的應用。容器類主要包括:NSDictionary、NSArray、NSSet三種。關于容器類的操作方法有很多,分類整理一下
1、如果作為對象的一個屬性值,那就作為對象屬性處理,無論Key還是KeyPath都符合前四條中說的規則;
2、就可變不可變來說,一般來說存什么取什么,但是可以根據需要獲取相應的方法
@property (nonatomic,assign)NSMutableArray *mutableArray;
@property (nonatomic,assign)NSArray *array;
- (void)viewDidLoad {
[super viewDidLoad];
[self setValue:[NSArray arrayWithObjects:@"zwq", nil] forKey:@"array"];
[self setValue:[NSMutableArray arrayWithObjects:@"zwq2", nil] forKey:@"mutableArray"];
NSLog(@"不可變:%@--%@",[[self valueForKey:@"array"] class],[[self mutableArrayValueForKey:@"array"] class]);
NSLog(@"可變:%@--%@",[[self valueForKey:@"mutableArray"] class],[[self mutableArrayValueForKey:@"mutableArray"] class]);
}
輸出結果
2016-09-01 16:30:55.057 KVC[3328:231529] 不可變:__NSArrayI--NSKeyValueSlowMutableArray
2016-09-01 16:30:55.057 KVC[3328:231529] 可變:__NSArrayM--NSKeyValueSlowMutableArray
//KeyPath道理也是一樣的
3、需要單獨說的是NSDictionary跟NSArray有點不一樣,而且功常用一點
//根據指定dic設置對象屬性值。使用dic的key來標識屬性,dic的value標識值,底層調用setValue:forKey:進行賦值。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
//獲取一組key的屬性值,然后以NSDictionary形式返回
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
一個常見的功能應用,獲取網絡數據,數據解析完畢然后賦值的時候,如果Key很多是個很麻煩的事情,但是使用setValuesForKeysWithDictionary:
一行代碼搞定
//比如Model的屬性
@property (nonatomic,copy)NSString *name;
@property (nonatomic,copy)NSString *address;
- (void)viewDidLoad {
[super viewDidLoad];
//比如需要解析的數據
NSDictionary *dic =@{@"name":@"zwq",@"address":@"地球"};
[self setValuesForKeysWithDictionary:dic];
NSLog(@"name:%@--address:%@",self.name,self.address);
}
輸出結果
2016-09-01 16:42:47.898 KVC[3367:237574] name:zwq--address:地球
注意:
1、如果dic中有未定義的key那么需要進行異常處理,參考《三、異常處理》段落。
2、容器類比如NSArray, NSSet, NSDictionary不能包含nil值,需要使用NSNull替換(一個表示nil值的單例類)
3、方法dictionaryWithValuesForKeys:和setValuesForKeysWithDictionary:會自動轉換NSNull和nil,不需要過多關注。
4、容器類運算符
容器類運算是valueForKeyPath:
中特殊的KeyPath,運算符跟在@符號之后,格式如下圖
整個KeyPath以運算符為中心,分為3部分。左邊的路徑標識容器類(set或者array)的訪問路徑,中間是運算符,右邊是參加運算的屬性訪問路徑。
暫不支持自定義運算符,總體分為三種;
分類 | 內容 |
---|---|
基本運算符 | @avg(平均值)、@count(數量)、@max(最大值)、 @min(最小值)、@sum(求和) |
對象運算符 | @distinctUnionOfObjects(祛同屬性值集合)、@unionOfObjects(屬性值集合) |
容器運算符 | @distinctUnionOfArrays()、@unionOfArrays()、@distinctUnionOfSets() |
選擇其中一個演示一下,其它的運算符同理。
//VC有一個數組屬性
@property (nonatomic,assign)NSArray *array;
- (void)viewDidLoad {
[super viewDidLoad];
//Data有一個name屬性
Data *data1 = [[Data alloc] init];
Data *data2 = [[Data alloc] init];
Data *data3 = [[Data alloc] init];
data1.name=@"data1";
data2.name=@"data2";
data3.name=@"data3";
//self.array.name
NSArray *arr = [NSArray arrayWithObjects:data1,data2,data1, nil];
[self setValue:arr forKey:@"array"];
NSArray *distinctArr = [self valueForKeyPath:@"array.@distinctUnionOfObjects.name"];
NSLog(@"distinctArr:%@",distinctArr);
NSArray *undistinctArr = [self valueForKeyPath:@"array.@unionOfObjects.name"];
NSLog(@"undistinctArr:%@",undistinctArr);
}
輸出結果
2016-09-01 17:17:59.049 KVC[3507:256556] distinctArr:(
data1,
data2
)
2016-09-01 17:17:59.050 KVC[3507:256556] undistinctArr:(
data1,
data2,
data1
)
以上問本人自己學習感悟,理解并整理。更多內容請查看官方文檔。