KVC 是 Key-Value Coding的简称。是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议提供对其属性
的间接访问。
请注意:这里说的很明确,是访问属性的
KVC 缺点:
1、KVC是使用过程中 ,是直接使用字符串进行操作,编译器不会进行检查,不易发现错误。后期维护也可能出现问题。
2、有时候取值可能也不是我们想要的类型。(可以拉到最后看注意点里面的 1、2)。
结论(具体步骤)
KVC虽然是NSObject的方法(来自 NSObject的分类@interface NSObject(NSKeyValueCoding)
)。但是其实现在Foundation.framework
里,这里面是看不到源码的实现。
我们借助官方文档的介绍,先看结论
set(赋值)
1、找set<key>
、_set<Key>
、setIsName
(setIsName文档上没有,但是确实是调用了)的方法,如果有,调用找到的方法 并结束。
2、如果没找到,就去找accessInstanceVariablesDirectly
这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常(步骤4)。
3、在2满足的情况下 ,按照这个顺序_<key>
,_is<Key>
,<key>
,is<Key>
找成员变量,并将value赋值给这个成员变量。
4、如果上面的步骤都失败了,调用setValue:forUndefinedKey:
抛出异常。
详细流程如下图:get(取值) Object对象的流程
1、按顺序找get<Key>
、<key>
、is<Key>
、_<key>
的方法,如果有,调用找到的方法 并结束。
2、尝试获取一个NSArray:
2.1 、如果找到countOf<Key>
和objectIn<Key>AtIndex:
这2个方法,会根据这2个方法创建一个数组并返回
2.2、如果找到countOf<Key>
和<key>AtIndexes:
这2个方法,也同样会返回一个数组
3、尝试获取一个NSSet:
同时找到3个方法:countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
,如果找到,会返回一个NSSet
4、如果没找到,就去找accessInstanceVariablesDirectly
这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常
4、按照这个顺序_<key>
,_is<Key>
,<key>
,is<Key>
找成员变量,并取出该成员变量的值。
5、进行细节处理:
5.1、如果是object 指针,直接返回结果
5.2、如果是NSNumber支持的标量类型,存储NSNumber 并返回
5.3、如果结果是NSNumber不支持的标量类型,转换成NSValue对象并返回
6、如果上面的步骤都失败了,调用valueForUndefinedKey:
抛出异常。
验证步骤
官方文档有很详细的讲解,这里做记录和自己的补充理解。(官方文档是英文版,如果引文不太好的同学,可以借助Chrome自带的翻译功能,准确率。。。。。至少比看不懂英文要强一点。。。)。
set(赋值)
先看以下代码:
//定义一个NSObject类 Person
//添加一个公开的成员变量(@public 方便打印)
//添加一个NSString属性
//添加一个NSInteger属性
@interface Person : NSObject
{
@public
NSString *jeName;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
// 对其内容进行赋值
Person *p = [Person new];
[p setValue:@"nameValue" forKey:@"name"];
//[p setValue:@"nameValue" forKey:@"_name"];
[p setValue:@"jeNameValue" forKey:@"jeName"];
[p setValue:@4 forKey:@"age"];
NSLog(@"name = %@, jeName = %@ age = %@",p.name,p->jeName,@(p.age));
// 查看打印信息
name = nameValue, jeName = jeNameValue age = 4
从上面的代码中,有几个疑问:
- 明明是对属性进行访问,为何在KVC之后,成员变量jeName也有值了?
- NSInteger通过KVC赋值的是一个NSNumber对象
为了搞清问题,先了解KVC原理。
在文档的Search Pattern for the Basic Setter
部分中看到这样的介绍。
1、找
set<key>
、_set<Key>
的方法,如果有,调用找到的方法 并结束。
2、如果没找到,就去找accessInstanceVariablesDirectly
这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常(步骤4)。
3、在2满足的情况下 ,按照这个顺序_<key>
,_is<Key>
,<key>
,is<Key>
找成员变量,并将value赋值给这个成员变量。
4、如果上面的步骤都失败了,调用setValue:forUndefinedKey:
抛出异常。
accessInstanceVariablesDirectly:
方法含义解释:--- 是否直接访问成员变量---- 如果有类似的成员变量,可不可以让我给它赋值
什么意思呢? 用代码来看具体是怎样调用的。
- 步骤1的验证:
//定义一个NSObject类 Person implementation 中不做任何操作
// 添加一个Person的分类 Person+JE
// 在分类中添加一个 jeName属性 (在person中添加属性,会被编译器自动生成set、get方法,不便与研究set、get流程)
@interface Person (JE)
@property (nonatomic, strong) NSString *jeName;
@end
@implementation
#pragma mark - 关闭或开启实例变量赋值 (不实现的情况下,默认返回YES)
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
//set方法1
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
//set方法2
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
//set方法3 --------- 这个方法官方文档上面没有提,但是确实是调用了
- (void)setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
@end
//对jeName 用KVC进行赋值
Person *p = [Person new];
[p setValue:@"jeNameValue" forKey:@"jeName"];
NSLog(@"jeName = %@",p->jeName);
步骤1的验证 做以下操作
操作 | 3个set方法都保留 | 屏蔽set1 | 屏蔽set1、2 | 3个set都屏蔽 |
---|---|---|---|---|
打印方法 | 打印方法1 | 打印方法2 | 打印方法3 | - |
jeName 值 | nil | nil | nil | jeNameValue |
jeName 值的原因 | 对set进行覆盖,但是set内未赋值 | 同左 | 同左 | 未实现set方法,进行了步骤3 |
这样可以验证系统是按照set<key>
、_set<key>
、setIs<key>
的步骤去走set方法
- 步骤3的验证:
用以下代码:
// 只有Person类,不创建分类
@interface Person : NSObject
{
@public //方便外部打印
NSString *_jeName; //成员变量1
NSString *_isJeName; //成员变量2
NSString *jeName; //成员变量3
NSString *isJeName; //成员变量4
}
@end
.m 文件不做任何操作
//外部KVC赋值 并打印
Person *p = [Person new];
[p setValue:@"jeNameValue" forKey:@"jeName"];
//[p setValue:@4 forKey:@"age"];
NSLog(@"_jeName = %@ _isJeName = %@ jeName = %@ isJeName = %@",p->_jeName,p->_isJeName,p->jeName,p->isJeName);
打印结果
_jeName = jeNameValue _isJeName = (null) jeName = (null) isJeName = (null)
步骤3操作
操作 | 4个成员变量都存在 | 屏蔽成员变量1 | 屏蔽成员变量1、2 | 屏蔽成员变量1、2、3 | 屏蔽成员变量1、2、3 、4 |
---|---|---|---|---|---|
分别打印成员变量值 | 变量1有值 | 变量2 有值 | 变量3 有值 | 变量4 有值 | 抛出异常 |
走到这里就知道为何可以对成员变量进行赋值了。其实是因为 系统在查找3个set方法没找到情况下,走了步骤3,对类似的成员变量进行了赋值
get(取值)
还是先看文档在文档的
Search Pattern for the Basic Getter
部分中看到这样的介绍。
1、按顺序找
get<Key>
、<key>
、is<Key>
、_<key>
的方法,如果有,调用找到的方法 并结束。
2、如果没找到,就去找accessInstanceVariablesDirectly
这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常4。
3、在2满足的情况下 ,按照这个顺序_<key>
,_is<Key>
,<key>
,is<Key>
找成员变量,并取出该成员变量的值。
4、如果上面的步骤都失败了,调用valueForUndefinedKey:
抛出异常。
用下面代码验证
- 验证步骤1
// 创建一个Person类并什么代码都不写
// 创建一个Person的分类 Person+JE
// 在Person+JE.m 中添加以下4个方法
@implementation Person (JE)
//方法1
- (NSString *)getJeName {
NSLog(@"%s ",__func__);
return @"";
}
//方法2
- (NSString *)jeName {
NSLog(@"%s ",__func__);
return @"";
}
//方法3
- (NSString *)isJeName {
NSLog(@"%s ",__func__);
return @"";
}
//方法4
- (NSString *)_jeName {
NSLog(@"%s ",__func__);
return @"";
}
@end
都不屏蔽 //打印方法1
屏蔽方法1 // 打印方法2
方法1、2 // 打印方法3
方法1、2、3 // 打印方法4
方法1、2、3、4 // 抛出异常 valueForUndefinedKey
- 步骤3的验证
创建Person 并添加4个成员变量
@interface Person : NSObject
{
@public //方便外部打印
NSString *_jeName; //成员变量1
NSString *_isJeName; //成员变量2
NSString *jeName; //成员变量3
NSString *isJeName; //成员变量4
}
@end
外部对4个变量赋值 并通过KVC对jeName 进行取值
Person *p = [Person new];
p->_jeName = @"_jeName";
p->_isJeName = @"_isJeName";
p->jeName = @"isJeName";
p->isJeName = @"isJeName";
NSString *kvcValue = [p valueForKey:@"jeName"];
NSLog(@"jeName = %@",kvcValue);
可以分别屏蔽 成员变量1、2、3、4 查看打印结果,来验证步骤3。
KVC补充
1、keyPath
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
是KVC 一级/多级
赋值/取值
的一种方式。
比如以下代码:Person里面有一个 model 属性
// Person : NSObject
//TestModel : NSObject
@interface Person : NSObject
@property (nonatomic, strong) TestModel *model;
@end
@interface TestModel : NSObject
@property (nonatomic, strong) NSString *modelName;
@end
可以直接通过keyPath的方式对model属性里面的 modelName进行赋值
Person *p = [Person new];
[p setValue:[TestModel new] forKey:@"model"];
// 等同 p.model = [TestModel new];
// 等同 [p setValue:[TestModel new] forKeyPath:@"model"];
[p setValue:@"modelName" forKeyPath:@"model.modelName"];
NSLog(@"modelName = %@",p.model.modelName);
keyPath 注意点:
1、比必须保证赋值的上一级是有值的(内存不为空)否则赋值无效。
2、可以对一级、多级赋值(上面代码中,初始化model属性的时候,forKey:
或者forKeyPath:
都是可行的)
3、forKey
只能对一级进行赋值(比如上面代码中model中的modelName进行初始化的时候,用forKey的方式的话 会报错)
- keyPath 还有一个用法:
将数组中的所有元素的某一个属性拿出来放在一个数组中
// 上面代码中的 TestModel
NSMutableArray *tempArr = [NSMutableArray new];
for (NSInteger i = 0; i < 3; i ++) {
TestModel *model = [TestModel new];
[tempArr addObject:model];
}
// NSArray *arr = [tempArr valueForKey:@"modelName"]; 和下面的 valueForKeyPath 效果相同
NSArray *arr = [tempArr valueForKeyPath:@"modelName"];
//Arr 的打印结果
<__NSArrayI 0x6000020d0cf0>(
<null>,
<null>,
<null>
)
2、集合操作符
集合操作符是在调用[valueForKeyPath:]
根据keypath中的特定符号,返回数据之前以某种方式对数据操作,官方文档戳这里
集合操作符的标准写法:
[ obj valueForKeyPath:@"leftKeypath.@collectionOperator.rightKeyPath"]
当操作对象本身是NSArray类型的时候,leftKeyPath可以省略
举例:
//创建一个Model,有一个NSArray类型的属性
@interface TestModel : NSObject
@property (nonatomic, strong) NSMutableArray<TestChildModel *> *childArr;
@end
@interface TestChildModel : NSObject
@property (nonatomic, assign) NSInteger childAge;
@end
// 创建一个TestModel 并对其进行内容赋值
//下面2种写法都是一样的
id obj1 = [model valueForKeyPath:@"childArr.@sum.childAge"]; //操作对象是NSObject,那么leftKeyPath 就不能省略
id obj2 = [model.childArr valueForKeyPath:@"@sum.childAge"]; //操作对象是model.childArr 数组类型,那么leftKeyPath 就能省略,而且必须要省略
集合操作符包括:
Aggregation Operators
聚合操作符
@avg
平均数,@count"
计数,@max
最大值,@min
最小值,@sum
求和Array Operators
数组操作符
1、@unionOfObjects
操作对象指定属性的集合,类似上面说的NSArray *arr = [tempArr valueForKeyPath:@"modelName"];
2、@distinctUnionOfObjects
操作对象指定属性去重后的集合 类似于unionOfObjects
,不过多了一步去重的操作Nesting Operators
嵌套操作符
1、unionOfArrays
操作嵌套对象指定属性的集合,操作对象是指定嵌套对象,比如二位数组(数组里面的对象也是数组),将2个数组中指定的属性全部组合在一起
2、distinctUnionOfArrays
把 unionOfArrays 的结果进行去重操作、
3、distinctUnionOfSets
操作的对象是嵌套的 NSSet, 得到的结果是一个去重的 NSSet(NSSet本身就有去重的特性)
⚠️:对NSSet 进行 valueForKey: 操作时,得到的结果是一个NSSet,而且NSSet的count 并不一定和原来的操作对象的count相同,因为NSSet 有去重特性
KVC 细节(注意点)
- 1、在上面取值过程中,说到如果没找到相关的get方法,那么会直接取值相似的成员变量并返回,以下代码中:取值的类型是什么? 值是什么?
//创建一个Person类 并添加一个公开成员变量
@interface Person : NSObject {
@public //方便外部赋值、取值
NSString *_age;
}
//创建一个Person分类 Person+JE 并添加一个属性age
@interface Person (JE)
@property (nonatomic, assign) NSInteger age;
@end
//在外部进行赋值并打印valueForKey:
Person *p = [Person new];
p->_age = @"1";
id age = [p valueForKey:@"age"];
NSLog(@"age = %@",age);
打印结果是 age = "1"; age是NSString类型。
因为是直接取到_age的值并返回,所以,即使定义的是NSInteger 类型,最后运行时,得到的依然是NSString
- 2、依然是上面的代码,在外部进行通过KVC对age进行赋值
Person *p = [Person new];
[p setValue:@12 forKey:@"age"];
id age = [p valueForKey:@"age"];
NSString *_age = p->_age;
NSLog(@"age = %@ cls = %@ \
_age = %@ cls = %@",age,[age class],_age,[_age class]);
可以先猜想打印结果。
其实这里的age 和 _age 最终的取值 都是来自Person中的公开成员变量 NSString *_age
age = 12 cls = __NSCFNumber _age = 12 cls = __NSCFNumber
- 3、创建一个Person类并添加一个age属性和name(和上面的1、2已经没关系了)
@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) NSString *name;
@end
//外部进行赋值操作
Person *p = [Person new];
// [p setValue:@"a" forKey:@"age"];
[p setValue:@"12" forKey:@"age"];
[p setValue:@(13) forKey:@"name"];
猜测能否正常运行,如果正常运行,得到的age值是什么类型?
能正常运行
1、最后得到的age 还是 NSInteger(本身定一个属性的类型),值为12
2、最后的name 是 NSnumber 类型的13
因为在get取值步骤中,只说道,对NSnunber类型的属性有自动转换类型的功能
,但是并没有对其他类型的属性自动转换,所有age被自动转换为NSInteger,而name由于运行时机制,最后得到的是NSInteger
如果赋值为@"a",结果得到的age = 0; 因为@"a"转成integer类型就是0
也就是说。KVC赋值 具有自动转换类型的功能
。符合get流程中的5.2
- 4、 NSMutableDictionary 中使用
setValue: forKey:
和valueForKey:
系统对NSMutableDictionary的 setValue: forKey:
和NSDictionary 的valueForKey:
进行了重写
@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)
/* Return the result of sending -objectForKey: to the receiver.
*/
- (nullable ObjectType)valueForKey:(NSString *)key;
@end
@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)
/* Send -setObject:forKey: to the receiver, unless the value is nil, in which case send -removeObjectForKey:.
如果赋值为nil, 将会调用 removeObjectForKey 方法
*/
- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;
@end
所以 :在发送网络请求的时候,组装一个NSMutableDictionary 时,会有这样的写法:[dic setValue:@"value" forKey:@"key"];
来达到为dictionary添加一个key的目的。