一、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
)
以上问本人自己学习感悟,理解并整理。更多内容请查看官方文档。