定义
KVC(全称key-value coding)键值编码。在iOS开发中,允许开发者通过key直接访问对象的属性,或者给对象的属性进行赋值,而不需要调用明确的存取方法。这样就可以在运行时动态的访问和修改对象的属性,而不是在编译时确定。
KVC的定义是通过对NSObject的扩展来实现的,定义在NSKeyValueCoding.h文件中,是一个非正式协议。
KVC相关方法
在NSKeyValueCoding中,KVC最为重要的方法如下:1
2
3
4
5
6
7
8
9
10
11// 通过key来取值
- (id)valueForKey:(NSString *)key;
// 通过keyPath来取值
- (id)valueForKeyPath:(NSString *)keyPath;
// 通过key来设值
- (void)setValue:(id)value forKey:(NSString *)key;
// 通过keyPath来设值
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
NSKeyValueCoding中还有其他的相关方法,例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14// KVC提供属性值确认的API,它可以用来检查set的值是否正确,为不正确的值做一个替换值或者拒绝设值新值并返回错误原因
- (BOOL)validateValue:(inout id _Nullable *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable *)outError;
// 如果key不存在,且没有KVC无法搜索到任何和key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
// 和上一个方法一样,上一个方法为设值,该方法为取值
- (id)valueForUndefinedKey:(NSString *)key;
// 如果在setValue方法时给value传nil,则会调用该方法
- (void)setNilValueForKey:(NSString *)key;
// 输入一组key,返回该组key对应的value,再转成字典返回,用于将model转字典
- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
寻找key的策略
setValue:forKey:方法赋值的原理
设值会调用setValue:forKey:
方法,其大致步骤如下流程图所示:
- 查找
set<Key>:
或_set<Key>:
命名的setter,按照这个顺序,如果找到,则调用这个方法并将值传进去。 - 如果没有发现一个简单的setter,但是
accessInstanceVariablesDirectly
类属性返回YES,则查找一个命名规则为_key、_isKey、key、isKey的实例变量。按照这个顺序,如果查找到则将value赋值给实例变量。 - 如果没有找到setter或实例变量,则调用
setValue:forUndefinedKey:
方法,并默认抛出一个异常。
valueForKey:方法取值的原理
当调用valueForKey:
方法时,KVC对key的搜索顺序有点不同于setValue:forKey:
方法,大致步骤如下:
- 首先按
get<Key>
、<key>
、is<Key>
的顺序查找getter方法,找到直接调用。- 若方法的返回结果类型是一个对象指针,则直接返回结果。
- 若类型为能够转化为NSNumber的基本数据类型,转换为NSNumber后返回;否则转换为NSValue返回。
若上面的getter没有找到,则查找
countOf<Key>
、objectIn<Key>AtIndex:
、<Key>AtIndexes
格式的方法。
如果countOf<Key>
和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的集合代理。发送给这个代理集合的NSArray消息方法,就会以countOf<Key>
、objectIn<Key>AtIndex:
、<Key>AtIndexes
这几个方法组合的形式调用。如果receiver的类实现了get<Key>:range:
方法,该方法也会用于性能优化。还没查到,那么查找
countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
格式的方法。如果这3个方法都找到,那么久返回一个可以相应NSSet所有方法的集合代理。发送给这个代理集合的NSSet消息方法,就会以countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
组合的形式调用。还是没查到,那么如果类方法
accessInstanceVariablesDirectly
返回YES,那么按_<key>
、_is<Key>
、<key>
、is<Key>
的顺序直接搜索实例变量。如果搜索到了,则返回receiver相应实例变量的值。再没有查到,调用
valueForUndefinedKey:
方法,抛出异常。
使用keyPath
在实际开发过程中,一个类的成员变量有可能是自定义类或者其他的复杂数据类型,我们可以先用KVC获取该属性,然后再用KVC来获取这个自定义类的属性。但这样比较繁琐,因此KVC提供了一个解决方案,keyPath。1
2- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
处理异常
使用KVC过程中最常见的异常就是不小心使用了错误的key,或者在设值时不小心传了nil的值,KVC有特定的方法处理这些异常。
- KVC处理nil异常,如果在设值过程中,不小心传了nil值,KVC会调用方法
setNilValueForKey:
,这个默认方法是抛出NSInvalidArgumentException
异常,所以一般而言最好重写这个方法,对异常进行处理。 - KVC处理UndefinedKey异常,如果在设值取值传的key不存在时,程序就会crash,设值会调用到
setValue:forUndefinedKey:
方法,而取值会调用valueForUndefinedKey:
方法,这两个方法默认都是抛出NSUndefinedKeyException
异常,因此如果要避免程序crash,可以重写这两个方法。
集合类运算
集合运算符格式
KVC提供的valueForKeyPath:
方法非常强大,可以通过该方法对集合对象进行“深入”操作,在其keyPath中嵌套集合运算符,例如求一个数组中对象某个属性的count。集合运算符的格式如下:1
keyPathToCollection.@collentionOperator.keyPathToproperty
- keyPathToCollection:Left key path,要操作的集合对象,若调用
valueForKeyPath:
方法的对象本来就是集合对象,则可以为空。 - collectionOperator:Collection operator,集合操作符,一般以@开头。
- keyPathToproperty:Right key path,要运算的属性。
集合运算符的分类
集合运算符主要分为以下三类:
- 集合操作符:处理集合包含的对象,并根据操作符的不同返回不同的类型,返回值以NSNumber为主。
- 数组操作符:根据操作符的条件,将符合条件的对象包含在数组中返回。
- 嵌套操作符:处理集合对象中嵌套其他集合对象的情况,返回结果也是一个集合对象。
集合操作符
为了演示集合操作符,我们新建一个项目,定义一个Book类,有bookName和bookPrice属性,然后在main函数中,新建一个Book数组,再对数组进行集合操作。详细操作如下:
@avg
用来计算集合中right keyPath
指定的属性的平均值。1
2NSNumber *avgNum = [bookrack valueForKeyPath:@"@avg.bookPrice"];
NSLog(@"avg: %f", [avgNum floatValue]);@count
用来计算集合中对象的数量。注意:@count操作符不需要写rightKeyPath,如果写了也会被忽略。1
2NSNumber *count = [bookrack valueForKeyPath:@"@count"];
NSLog(@"count: %f", [count floatValue]);@sum
用来计算集合中right keyPath
指定的属性的总和。1
2NSNumber *sum = [bookrack valueForKeyPath:@"@sum.bookPrice"];
NSLog(@"sum: %f", [sum floatValue]);@max
用来查找集合中right keyPath
指定属性的最大值。1
2NSNumber *max = [bookrack valueForKeyPath:@"@max.bookPrice"];
NSLog(@"max: %f", [max floatValue]);@min
用来查找集合中right keyPath
指定属性的最小值。1
2NSNumber *min = [bookrack valueForKeyPath:@"@min.bookPrice"];
NSLog(@"min: %f", [min floatValue]);
数组操作符
@unionOfObjects
将集合中的所有对象的同一个属性放在数组中返回。1
2NSArray *priceArray = [bookrack valueForKeyPath:@"@unionOfObjects.bookPrice"];
NSLog(@"unionOfObjects: %@", priceArray);@distinctUnionOfObjects
将集合中对象的属性进行去重后并返回。1
2NSArray *nameArray = [bookrack valueForKeyPath:@"@distinctUnionOfObjects.bookName"];
NSLog(@"distinctUnionOfObjects: %@", nameArray);
需要注意:以上两个方法,如果操作的属性为nil,则在添加到数组中时会导致crash。
嵌套操作符
由于嵌套操作符是需要对嵌套的集合对象进行操作,所以新建了一个racks数组,其中包含了两个Book类型对象的数组。
@unionOfArrays
是用来操作集合内部的集合对象,将所有right keyPath
对应的对象放在一个数组中返回。1
2NSArray *unionArray = [racks valueForKeyPath:@"@unionOfArrays.bookName"];
NSLog(@"unionOfArrays: %@", unionArray);@distinctUnionOfArrays
是用来操作集合内部的集合对象,将所有right keyPath
对应的对象放在一个数组中,并进行去重后返回。1
2NSArray *distinctArray = [racks valueForKeyPath:@"@distinctUnionOfArrays.bookPrice"];
NSLog(@"distinctUnionOfArrays: %@", distinctArray);
KVC 安全性检查
在使用KVC时,由于传入的key或者keyPath是一个字符串,因此很容易写错或者属性本身修改后忘记修改对应的字符串,导致crash。
解决的方案为,利用反射机制,通过@selector()
获取到方法的SEL,然后通过NSStringFromSelector()
将SEL反射为字符串。这样在@selector()
中传入方法名的过程中,编译器会有合法性检查,如果方法不存在或者未实现时,会报对应的警告。1
[self valueForKey:NSStringFromSelector(@selector(object))];