iOS KVC小结
KVC的概念
KVC,即Key-value coding,键值编码。给我们提供了一套更加直接的方式,来访问一个对象的属性,或者给对象的属性赋值。而不是通过setter和getter方法。是一种可以直接通过名字或者key来获取对象的属性的机制。KVC的功能很强大,也属于iOS的黑魔法之一。类的只读属性或者私有属性(iOS 13以后,一些控件的私有属性就不能获取了,运行时会报错;但是自定义的类,用代码测试还可以使用),只要能获取到属性的名字,使用KVC都可以进行值的操作。
成员变量、属性
开始之前,先来回忆一下成员变量、属性的概念,以便于对后面的KVC的流程有更清楚的理解。
@interface KVCPerson : NSObject
{
//成员变量
NSString * _name;
}
//属性
@property (nonatomic, copy) NSString * address;
@end
创建了一个KVCPerson类,代表一个人。一个人有名字、家庭住址。所以我们把名字添加为“成员变量”,把家庭住址添加为“属性”。
-
在大括号
{}
中声明的变量都是成员变量。成员变量不会自动生成setter
、getter
方法。- 成员变量的取值/赋值,使用
->
。例如:p->name
- 成员变量的取值/赋值,使用
-
OC中用
@property
来声明一个属性,编译器会自动为属性生成“成员变量”,并且为成员变量生成对应的setter
和getter
方法。属性可以使用“点语法”进行操作。例如:
p.name
获取KVCPerson类的IvarList和MethodList展示如下:
//成员变量列表
mIvarList ==
(
"_name",
"_address"
)
//方法列表
mMthodList ==
(
getIvarList,
getMethodList,
//自动生成的address的getter方法
address,
//自动生成的address的setter方法
"setAddress:",
init,
".cxx_destruct"
)
KVC常用的API
设置值的方法
//给定一个值和一个属性的键名,设置该属性的值。
- (void)setValue:(nullable id)value forKey:(NSString *)key;
//给定一个值和一个“属性的属性”的键名,设置该“属性的属性”的值。like:address.city。
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
//处理设置值的时候没有找到键名的异常情况的API。找不到对应 key 命名的属性时,就会 NSUnknownKeyException 异常崩溃,可以在对象里重写下面两个方法,防止崩溃。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//如果将非对象类型的属性设置为 nil的情况下,会报NSInvalidArgumentException 的异常;可以使用这个方法来处理异常,防止崩溃
- (void)setNilValueForKey:(NSString *)key;
在给属性赋值的时候,KVC不会像setter
、getter
方法那么温柔,对于类的只读属性或者私有属性,setter
和getter
是访问不到的。
相比之下,KVC就会显得比较粗暴。一个类的只读属性、私有属性,都可以进行赋值操作。
获取值的方法
//给定一个键名,获取到该键对应的值
- (nullable id)valueForKey:(NSString *)key;
//给一个“属性的属性”的键名,获取到该“属性的属性”的值。这个方法结合操作符做到一些比较好玩的事情
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//获取值操作的情况下,没有找到键名,会抛出异常。在类中可以重写这个方法,防止程序的崩溃。
- (nullable id)valueForUndefinedKey:(NSString *)key;
没有setter
/getter
等方法情况下,根据这个方法的返回决定是否直接查询成员变量
//在找不到属性的setter/getter方法的情况下,是否直接去查询属性的成员变量。默认返回yes
+ (BOOL)accessInstanceVariablesDirectly;
属性值正确性验证
//属性的正确性验证,默认返回yes。可以对一个属性进行验证,如果符合逻辑条件就可以返回yes,否则返回false。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
使用案例:
验证年龄的合理性,如果年龄超过200岁就不合情理了,所以可以对年龄加上正确性验证。在KVCPerson类中增加一个属性age
,并添加验证代码。
//正确性验证
- (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类的时候,添加年龄的验证逻辑代码
KVCPerson * p = [[KVCPerson alloc] init];
NSNumber * value = @200;
if ([p validateValue:&value forKey:@"age" error:NULL]) {
NSLog(@"我的数据不正确");
}
KVC赋值的流程顺序
在调用- (void)setValue:(nullable id)value forKey:(NSString *)key
方法的时候,执行的流程如下:
-
查询类中的有没有相关的方法实现:
查询类中有没有
set<key>:(NSString *)key
的方法。有的话通过setter
方法给属性设值;否则继续查找。查询类中有没有
_set<key>:(NSString *)key
的方法。有的话通过setter
方法给属性设值;否则继续查找。查询类中有没有
setIs<key>:(NSString *)key
的方法。有的话通过setter
方法给属性设值;没有的话,需要去查询accessInstanceVariablesDirectly
的返回值。
-
没有查询到上述的方法的情况下,会去查询
accessInstanceVariablesDirectly
这个方法的返回值(没有重写的话,默认返回YES)。如果该方法的返回值是NO的情况下,会抛出
NSUnknownKeyException
异常如果该方法的返回值是YES的情况下,会去找类中的成员变量,并且按照一定的优先级顺序先匹配成员变量。获取到匹配的成员变量后,直接赋值。
-
没有找到对应的
setter
方法,直接给成员变量赋值的流程按照优先级顺序进行匹配成员变量:
_<key>
>_is<Key>
><key>
>is<Key>
按照顺序查询到一个能匹配的成员变量后,就给该成员变量赋值。
假设以上四种形式的成员变量都存在,只匹配优先级最高的一个。
流程图显示如下:
KVC取值的流程顺序
调用- (nullable id)valueForKey:(NSString *)key
方法的时候,执行的流程如下:
-
查询类的方法列表中有没有相关的
getter
方法实现查询类中有没有
- (NSString *)get<Key>
方法。有的话通过getter
方法取出属性的值;否则继续查找。查询类中有没有
- (NSString *)<key>
方法。有的话通过getter
方法取出属性的值;否则继续查找。查询类中有没有
- (NSString *)is<Key>
方法。有的话通过getter
方法取出属性的值;否则继续查找。查询类中有没有
- (NSString *)_<key>
方法。有的话通过getter
方法取出属性的值;没有的话,需要去查询accessInstanceVariablesDirectly
的返回值。
-
没有查询到上述的方法的情况下,会去查询
accessInstanceVariablesDirectly
这个方法的返回值(没有重写的话,默认返回YES)如果该方法的返回值是NO的情况下,会抛出
NSUnknownKeyException
异常如果该方法的返回值是YES的情况下,会去找类中的成员变量,并且按照一定的优先级顺序先匹配成员变量。获取到匹配的成员变量后,取出该成员变量的值。
-
没有找到对应的
getter
方法,就需要直接查询匹配的成员变量,然后取出该成员变量的值。按照优先级顺序进行匹配成员变量:
_<key>
>_is<Key>
><key>
>is<Key>
按照顺序查询到一个能匹配的成员变量后,就取出该成员变量的值。
假设以上四种形式的成员变量都存在,只匹配优先级最高的一个。
流程图显示如下:
KVC集合操作符
在学习KVC的时候,看到了KVC集合操作符,眼前一亮啊。所以,为了以后能够随时看一下,在这里进行记录。
KVC集合操作符允许在valueForKeyPath:方法中使用操作运算,作用于集合中所有的元素,来获取到想要的成果。
集合操作符根据其返回值的不同,分为三个类型:
- 简单的集合操作符,返回的是 strings, number, 或者 dates
* @count: 返回的值为集合中对象总数,是`NSNumber`类型数据。
* @sum : 首先把集合中的每个对象都转换为`double`类型,然后计算其总和,最后返回值为这个总和的`NSNumber`对象。
* @avg : 把集合中的每个对象都转换为`double`类型,然后计算其平均值,返回值为平均值的`NSNumber`对象。
* @max : 使用`compare:`方法来确定最大值。所以为了让其正常工作,集合中所有的对象都必须支持和另一个对象的比较。
* @min : 和`@max`一样,但是返回的是集合中的最小值。
- 对象操作符,返回的是一个数组
@unionOfObjects / @distinctUnionOfObjects: 返回一个由操作符右边的 key path 所指定的对象属性组成的数组。两个方法中@distinctUnionOfObjects会对数组去重, 而且@unionOfObjects不会.</pre>
- 数组和集合操作符, 返回的是一个数组或者集合
@distinctUnionOfArrays / @unionOfArrays: 返回了一个数组,其中包含这个集合中每个数组对于这个操作符右面指定的 key path 进行操作之后的值。正如你期望的,distinct版本会移除重复的值。
@distinctUnionOfSets: 和@distinctUnionOfArrays差不多, 但是它期望的是一个包含着NSSet对象的NSSet,并且会返回一个NSSet对象。因为集合不能包含重复的值,所以它只有distinct操作。</pre>
事例数据:
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 |
创建一个类,类名为Product,如下。把以上数据包装成对象,保存到productArray
数组中。
@interface Product : NSObject
//产品名称
@property (nonatomic, copy) NSString * name;
//产品价格
@property (nonatomic, assign) int price;
//产品的上市时间
@property (nonatomic, strong) NSDate * date;
@end
- 简单的集合操作符,应用实例:
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
- 对象操作符,应用实例(在
productArray
数组中,使用第一条数据重复生成多次对象,能看到两个方法的区别):
//@unionOfObjects方法,不会对数组去重
NSArray *unionOfObjects = [self.productArray valueForKeyPath:@"@unionOfObjects.name"];
//@distinctUnionOfObjects 会对数组去重
NSArray *distinctUnionOfObjects = [self.productArray valueForKeyPath:@"@distinctUnionOfObjects.name"];
//打印数据
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"
)
- 数组和集合操作符, 应用实例。(再复制一份商品数据,稍加修改后,包装成对象存放在一个数组中。并与存放上份数据的数组放在同一个数组中。)。集合运算符与数组操作法是相似的,此处不做赘述。
//把两份产品数据存放到同一个数组中
NSArray *arraysInArray = @[self.productArray, newProductArray];
//@unionOfArrays方法,不会对数据去重
NSArray *unionOfArrays = [arraysInArray valueForKeyPath:@"@unionOfArrays.name"];
//@distinctUnionOfArrays方法,会对获取到的数据去重
NSArray *distinctUnionOfArrays = [arraysInArray valueForKeyPath:@"@distinctUnionOfArrays.name"];
//打印数据
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
)