一、概述
KVC(Key-value coding)键值编码,iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定。
在实现了访问器方法的类中,使用点语法和KVC访问对象其实差别不大,二者可以任意混用。但是没有访问起方法的类中,点语法无法使用,这时KVC就有优势了。
二、KVC的定义与使用
1、KVC相关的方法
KVC的定义都是对NSObject的扩展来实现的,Objective-c中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject在类型,都能使用KVC,下面是KVC最为重要的四个方法:
- (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
当然NSKeyValueCoding类别中还有其他的一些方法,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+(BOOL)accessInstanceVariablesDirectly;
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
-(BOOL)validateValue:(inoutid__nullable *__nonnull)ioValueforKey:(NSString *)inKeyerror:(outNSError **)outError;
//KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
-(NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
-(nullableid)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
-(void)setValue:(nullableid)valueforUndefinedKey:(NSString *)key;
//和上一个方法一样,只不过是设值。
-(void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
-(NSDictionary<NSString *,id>*)dictionaryWithValuesForKeys:(NSArray<NSString *>*)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
同时苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。
有序集合对应方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-countOf<Key>//必须实现,对应于NSArray的基本方法count:2 -objectIn<Key>AtIndex:
-<key>AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
-get<Key>:range://不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:
-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>://可选的,如果在此类操作上有性能问题,就需要考虑实现之
无序集合对应方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-countOf<Key>//必须实现,对应于NSArray的基本方法count:
-objectIn<Key>AtIndex:
-<key>AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
-get<Key>:range://不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:
-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>://这两个都是可选的,如果在此类操作上有性能问题,就需要考虑实现之
2、一对多关系(To-Many)中的集合访问器方法
我们平时大部分使用的属性都是一对一关系(To-One),比如Person类中的name属性,每个人只有一个名字。但也有一对多的关系,比如Person中有一个friendsName属性,这是个集合(在Objective-C中可以是NSArray,NSSet等),保存的是一个人的所有朋友的名字。
当操作一对多的属性中的内容时,我们有两种选择:
(1)间接操作
先通过KVC方法取到集合属性,然后通过集合属性操作集合中的元素。实际开发中最常用的方法。
(2)直接操作
苹果为我们提供了一些方法模板(即上面提到的有序集合和无序集合对应的方法),我们可以以规定的格式实现这些方法来达到访问集合属性中元素的目的。不过在实际开发中一般不使用直接操作的方法,使用间接操作就足够了,苹果甚至都没有让这些方法以哪怕是非正式协议的形式出现,而只是在编程指南中提了一下。
3、KVC对数值和结构体型属性的支持
KVC可以自动的将数值或结构体型的数据打包或解包成NSNumber或NSValue对象,以达到适配的目的。
举个例子,Person类有个NSInteger类型的age属性,如下:
// Person.m
#import "Person.h"
@interface Person ()
@property (nonatomic,assign) NSInteger age;
@end
@implementation Person
@end
(1)修改值
我们通过KVC技术使用如下方式设置age属性的值:
[personsetValue:[NSNumbernumberWithInteger:5]forKey:@"age"];
我们赋给age的是一个NSNumber
对象,KVC会自动的将NSNumber
对象转换成NSInteger
对象,然后再调用相应的访问器方法设置age的值。
(2)获取值
同样,以如下方式获取age属性值:
[person valueForKey:@"age"];
这时,会以NSNumber的形式返回age的值。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ViewController.m
#import "ViewController.h"
#import "Person.h"
@interfaceViewController()
@end
@implementationViewController
-(void)viewDidLoad{
[superviewDidLoad];
Person *person=[[Personalloc]init];
[personsetValue:[NSNumbernumberWithInteger:5]forKey:@"age"];
NSLog(@"age=%@",[personvalueForKey:@"age"]);
}
@end
输出结果:
2017-01-16 16:31:55.709 Test[28586:2294177] age=5
需要注意的是我们不能直接将一个数值通过KVC赋值的,我们需要把数据转为NSNumber和NSValue类型传入,那到底哪些类型数据要用NSNumber封装哪些类型数据要用NSValue封装呢?看下面这些方法的参数类型就知道了:
可以使用NSNumber的数据类型有:
+(NSNumber*)numberWithChar:(char)value;
+(NSNumber*)numberWithUnsignedChar:(unsignedchar)value;
+(NSNumber*)numberWithShort:(short)value;
+(NSNumber*)numberWithUnsignedShort:(unsignedshort)value;
+(NSNumber*)numberWithInt:(int)value;
+(NSNumber*)numberWithUnsignedInt:(unsignedint)value;
+(NSNumber*)numberWithLong:(long)value;
+(NSNumber*)numberWithUnsignedLong:(unsignedlong)value;
+(NSNumber*)numberWithLongLong:(longlong)value;
+(NSNumber*)numberWithUnsignedLongLong:(unsignedlonglong)value;
+(NSNumber*)numberWithFloat:(float)value;
+(NSNumber*)numberWithDouble:(double)value;
+(NSNumber*)numberWithBool:(BOOL)value;
+(NSNumber*)numberWithInteger:(NSInteger)valueNS_AVAILABLE(10_5,2_0);
+(NSNumber*)numberWithUnsignedInteger:(NSUInteger)valueNS_AVAILABLE(10_5,2_0);
就是一些常见的数值型数据。
可以使用NSValue的数据类型有:
+ (NSValue*)valueWithCGPoint:(CGPoint)point;
+ (NSValue*)valueWithCGSize:(CGSize)size;
+ (NSValue*)valueWithCGRect:(CGRect)rect;
+ (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue*)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);
NSValue
主要用于处理结构体型的数据,它本身提供了如上集中结构的支持。任何结构体都是可以转化成NSValue对象的,包括其它自定义的结构体。
三、KVC中使用KeyPath
在开发过程中,一个类的成员变量有可能是其他的自定义类,你可以先用KVC获取出来该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径KeyPath。
-(nullableid)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
-(void)setValue:(nullableid)valueforKeyPath:(NSString *)keyPath; //通过KeyPath来设值
KVC 同样允许我们通过关系来访问对象。假设 people
对象有属性 address
,address
有属性 country
,我们可以这样通过people 来访问country:
[people valueForKeyPath:@"address.country"];
值得注意的是这里我们调用 -valueForKeyPath:
而不是 -valueForKey:
。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//
// main.m
// TEST_OC
//
// Created by 李峰峰 on 2017/1/16.
// Copyright © 2017年 李峰峰. All rights reserved.
//
#import <Foundation/Foundation.h>
@interfaceAddress:NSObject
@end
@interfaceAddress()
@property(nonatomic,copy)NSString*country;
@end
@implementationAddress
@end
@interfacePeople:NSObject
@end
@interfacePeople()
@property(nonatomic,copy)NSString*name;
@property(nonatomic,strong)Address*address;
@property(nonatomic,assign)NSIntegerage;
@end
@implementationPeople
@end
intmain(intargc,constchar*argv[]){
@autoreleasepool{
People*people1=[Peoplenew];
Address*add=[Addressnew];
add.country=@"China";
people1.address=add;
NSString*country1=people1.address.country;
NSString *country2=[people1valueForKeyPath:@"address.country"];
NSLog(@"country1:%@ country2:%@",country1,country2);
[people1setValue:@"USA"forKeyPath:@"address.country"];
country1=people1.address.country;
country2=[people1valueForKeyPath:@"address.country"];
NSLog(@"country1:%@ country2:%@",country1,country2);
}
return0;
}
打印结果:
2017-01-16 17:17:42.443432 TEST_OC[29905:2372748] country1:China country2:China
2017-01-16 17:17:42.444862 TEST_OC[29905:2372748] country1:USA country2:USA
Program ended with exit code: 0
KVC键值验证(Key-Value Validation)
KVC提供了验证Key对应的Value是否可用的方法:
-(BOOL)validateValue:(inoutid*)ioValueforKey:(NSString*)inKeyerror:(outNSError**)outError;
该方法默认的实现是调用一个如下格式的方法:
- (BOOL)validate<Key>:error:
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@implementationAddress
-(BOOL)validateCountry:(id *)valueerror:(outNSError *_Nullable__autoreleasing *)outError{ //在implementation里面加这个方法,它会验证是否设了非法的value
NSString*country=*value;
country=country.capitalizedString;
if([countryisEqualToString:@"Japan"]){
returnNO; //如果国家是日本,就返回NO,这里省略了错误提示,
}
returnYES;
}
@end
NSError*error;
idvalue=@"japan";
NSString*key=@"country";
BOOLresult=[addvalidateValue:&valueforKey:keyerror:&error];//如果没有重写-(BOOL)-validate<Key>:error:,默认返回Yes
if(result){
NSLog(@"键值匹配");
[addsetValue:valueforKey:key];
}
else{
NSLog(@"键值不匹配");//不能设为日本,基他国家都行
}
NSString*country=[addvalueForKey:@"country"];
NSLog(@"country:%@",country);
打印结果:
2017-01-16 17:27:46.424332 TEST_OC[29905:2372748] 键值不匹配
2017-01-16 17:27:46.424332 TEST_OC[29905:2372748] country:China
这样就给了我们一次纠错的机会。需要指出的是,KVC是不会自动调用键值验证方法的,就是说我们如果想要键值验证则需要手动验证。但是有些技术,比如CoreData会自动调用。
上面的代码简单在展示了KeyPath是怎么用的。如果你不小心错误的使用了key而非KeyPath的话,KVC会直接查找address.country这个属性,很明显,这个属性并不存在,所以会再调用UndefinedKey相关方法。而KVC对于KeyPath是搜索机制第一步就是分离key,用小数点.来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去。接下来就来研究KVC是怎么寻找Key的。
四、用KVC中的函数操作集合
KVC同时还提供了很复杂的函数,主要有下面这些:
1、简单集合运算符
简单集合运算符共有@avg, @count , @max , @min ,@sum5
种,都表示什么直接看下面例子就明白了, 目前还不支持自定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//
// main.m
// TEST_OC
//
// Created by 李峰峰 on 2017/1/16.
// Copyright © 2017年 李峰峰. All rights reserved.
//
#import <Foundation/Foundation.h>
@interfaceBook:NSObject
@property(nonatomic,copy) NSString*name;
@property(nonatomic,assign) CGFloatprice;
@end
@implementationBook
@end
intmain(intargc,constchar*argv[]){
@autoreleasepool{
Book *book1=[Booknew];
book1.name=@"The Great Gastby";
book1.price=10;
Book *book2=[Booknew];
book2.name=@"Time History";
book2.price=20;
Book *book3=[Booknew];
book3.name=@"Wrong Hole";
book3.price=30;
Book *book4=[Booknew];
book4.name=@"Wrong Hole";
book4.price=40;
NSArray*arrBooks=@[book1,book2,book3,book4];
NSNumber*sum=[arrBooksvalueForKeyPath:@"@sum.price"];
NSLog(@"sum:%f",sum.floatValue);
NSNumber*avg=[arrBooksvalueForKeyPath:@"@avg.price"];
NSLog(@"avg:%f",avg.floatValue);
NSNumber*count=[arrBooksvalueForKeyPath:@"@count"];
NSLog(@"count:%f",count.floatValue);
NSNumber*min=[arrBooksvalueForKeyPath:@"@min.price"];
NSLog(@"min:%f",min.floatValue);
NSNumber*max=[arrBooksvalueForKeyPath:@"@max.price"];
NSLog(@"max:%f",max.floatValue);
}
return0;
}
运行结果:
2017-01-17 11:53:23.235907 TEST_OC[37115:3010741] sum:100.000000
2017-01-17 11:53:23.236161 TEST_OC[37115:3010741] avg:25.000000
2017-01-17 11:53:23.236227 TEST_OC[37115:3010741] count:4.000000
2017-01-17 11:53:23.236280 TEST_OC[37115:3010741] min:10.000000
2017-01-17 11:53:23.236322 TEST_OC[37115:3010741] max:40.000000
Program ended with exit code: 0
2、对象运算符
比集合运算符稍微复杂,能以数组的方式返回指定的内容,一共有两种:
@distinctUnionOfObjects
@unionOfObjects
它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//
// main.m
// TEST_OC
//
// Created by 李峰峰 on 2017/1/16.
// Copyright © 2017年 李峰峰. All rights reserved.
//
#import <Foundation/Foundation.h>
@interfaceBook:NSObject
@property(nonatomic,copy) NSString*name;
@property(nonatomic,assign) CGFloatprice;
@end
@implementationBook
@end
intmain(intargc,constchar*argv[]){
@autoreleasepool{
Book *book1=[Booknew];
book1.name=@"The Great Gastby";
book1.price=40;
Book *book2=[Booknew];
book2.name=@"Time History";
book2.price=20;
Book *book3=[Booknew];
book3.name=@"Wrong Hole";
book3.price=30;
Book *book4=[Booknew];
book4.name=@"Wrong Hole";
book4.price=10;
NSArray*arrBooks=@[book1,book2,book3,book4];
NSLog(@"distinctUnionOfObjects");
NSArray*arrDistinct=[arrBooksvalueForKeyPath:@"@distinctUnionOfObjects.price"];
for(NSNumber *priceinarrDistinct){
NSLog(@"%f",price.floatValue);
}
NSLog(@"unionOfObjects");
NSArray*arrUnion=[arrBooksvalueForKeyPath:@"@unionOfObjects.price"];
for(NSNumber *priceinarrUnion){
NSLog(@"%f",price.floatValue);
}
}
return0;
}
运行结果:
2017-01-17 11:57:11.606011 TEST_OC[37189:3016637] distinctUnionOfObjects
2017-01-17 11:57:11.606506 TEST_OC[37189:3016637] 10.000000
2017-01-17 11:57:11.606564 TEST_OC[37189:3016637] 20.000000
2017-01-17 11:57:11.606583 TEST_OC[37189:3016637] 30.000000
2017-01-17 11:57:11.606621 TEST_OC[37189:3016637] 40.000000
2017-01-17 11:57:11.606654 TEST_OC[37189:3016637] unionOfObjects
2017-01-17 11:57:11.606694 TEST_OC[37189:3016637] 40.000000
2017-01-17 11:57:11.606704 TEST_OC[37189:3016637] 20.000000
2017-01-17 11:57:11.606712 TEST_OC[37189:3016637] 30.000000
2017-01-17 11:57:11.606719 TEST_OC[37189:3016637] 10.000000
Program ended with exit code: 0
五、setValue:forKey:方法赋值的原理
例如对于:[item setValue:@”value” forKey:@”property”],具体实现为:
- 首先去模型中查找有没有setProperty,找到,直接调用赋值 [self setProperty:@”value”]
- 去模型中查找有没有property属性,有,直接访问属性赋值 property = value
- 去模型中查找有没有_property属性,有,直接访问属性赋值 _property = value
- 找不到,就会直接报错 setValue:forUndefinedKey:报找不到的错误
如果开发者想让这个类禁用KVC里,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set<Key>:属性名时,会直接用setValue:forUNdefinedKey:方法。
所以,我们使用KVC要有以下三个条件:
- 必须保证模型中定义的属性要大于或等于字典中key的数量。
- 模型中的基本数据类型无法进行转换。
- 属性的名字必须和键相同,否则找不到相关属性会报错。
六、KVC异常处理
KVC中最常见的异常就是不小心使用了错误的Key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常。
通常在用KVC操作Model时,抛出异常的那两个方法是需要重写的。虽然一般很小出现传递了错误的Key值这种情况,但是如果不小心出现了,直接抛出异常让APP崩溃显然是不合理的。一般在这里直接让这个Key打印出来即可,或者有些特殊情况需要特殊处理。
通常情况下,KVC不允许你要在调用setValue:属性值 forKey:@”name”(或者keyPath)时对非对象传递一个nil的值。很简单,因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。
[people1setValue:nilforKey:@"age"]
***Terminatingappduetouncaughtexception'NSInvalidArgumentException',reason:'[<People 0x100200080> setNilValueForKey]: could not set nil as the value for the key age.'// 调用setNilValueForKey抛出异常
如果重写setNilValueForKey:就没问题了:
@implementation People
-(void)setNilValueForKey:(NSString *)key{
NSLog(@"不能将%@设成nil",key);
}
@end
七、KVC和字典
当对NSDictionary对象使用KVC时,valueForKey:的表现行为和objectForKey:一样。所以使用valueForKeyPath:用来访问多层嵌套的字典是比较方便的。
KVC里面还有两个关于NSDictionary的方法:
-(NSDictionary<NSString *,id>*)dictionaryWithValuesForKeys:(NSArray<NSString *>*)keys;
-(void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id>*)keyedValues;
dictionaryWithValuesForKeys:
是指输入一组key,返回这组key对应的属性,再组成一个字典。
setValuesForKeysWithDictionary
是用来修改Model中对应key的属性。下面直接用代码会更直观一点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//
// main.m
// TEST_OC
//
// Created by 李峰峰 on 2017/1/16.
// Copyright © 2017年 李峰峰. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface Address : NSObject
@end
@interface Address()
@property (nonatomic,copy)NSString* country;
@property (nonatomic,copy)NSString* province;
@property (nonatomic,copy)NSString* city;
@property (nonatomic,copy)NSString* district;
@end
@implementation Address
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
//模型转字典
Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";
NSArray* arr = @[@"country",@"province",@"city",@"district"];
NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把对应key所有的属性全部取出来
NSLog(@"%@",dict);
//字典转模型
NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
[add setValuesForKeysWithDictionary:modifyDict]; //用key Value来修改Model的属性
NSLog(@"country:%@ province:%@ city:%@",add.country,add.province,add.city);
}
return 0;
}
打印结果:
2017-01-1622:53:36.200448TEST_OC[33394:2683077]{
city="Shen Zhen";
country=China;
district="Nan Shan";
province="Guang Dong";
}
2017-01-1622:53:36.202199TEST_OC[33394:2683077]country:USA province:californiacity:Losangle
Programendedwithexitcode:0
打印出来的结果完全符合预期。
0 0