关于KVC的详细介绍可以参考官方文档
键值编码(KVC)是由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象符合键值编码时,可以通过简洁,统一的消息传递接口通过字符串参数来访问其属性.
简单使用
需要遵守
NSKeyValueCoding
协议,直接或者间接继承自NSObject
的类苹果提供了默认实现.
- 使用键获取属性值
简单属性,值对象
- (nullable id)valueForKey:(NSString *)key
[person valueForKey :@"age"];
有层级(属性的属性)
- (nullable id)valueForKeyPath:(NSString *)keyPath
[person valueForKeyPath :@"address.street"];
批量读取,根据传入的一组
keys
,依次调用valueForKey
,返回一个以keys
每个key
为键的字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys
[person dictionaryWithValuesForKeys:@[@"name",@"age",@"hobby"]]
- 使用键设置属性值
注意:设置值的时候数据类型要和原来的一致,因为编译器不会提示这个错误
- (void)setValue:(nullable id)value forKey:(NSString *)key
[person setValue:@(18) forKey:@"age"];
有层级
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath
[person setValue:@"Sanlitun" forKeyPath:@"address.street"];
批量设置属性,这个方法会根据传入的字典参数,依次调用每个
key
的setValue:
方法
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues
[person setValuesForKeysWithDictionary:@{@"name":@"sanhe",
@"age":@(24),
@"hobby":@[@"羽毛球",@"高尔夫球"]
}];
- 集合类型
3.1以数组为例:
如果使用某个键值来访问一个数组(NSArray),他实际上会查询相应数组中的每个对象,然后将查询结果打包成另一个数组并返回给你。
使用valueForKey:和setValue:forKey:(或其等效键路径)的任何其他对象一样,获取或设置集合对象
NSArray *newArray = [p valueForKey:@"hobby"];
newArray = @[@"羽毛球",@"高尔夫球"];
[person setValue:newArray forKey:@"hobby"];
使用协议提供的方法mutableArrayValueForKey:
此方法的默认实现与-valueForKey:
识别相同的简单访问器方法和数组访问器方法,并遵循相同的直接实例变量访问策略,但是始终返回可变的集合代理对象,而不是-valueForKey:
的不可变集合
NSArray *newArray = [person mutableArrayValueForKey:@"hobby"];
newArray[1] = @"高尔夫球";
[person setValue:newArray forKey:@"hobby"];
如果有层级就用对应的keyPath方法
3.2集合操作符
对象调用valueForKeyPath:时,可以在KeyPath中嵌入一个集合操作符。集合操作符是一个小的关键字列表,前面有一个@,它指定了getter应该执行的操作,以便在返回数据之前以某种方式对数据进行操作。valueForKeyPath的默认实现由NSObject提供
1.集合运算符
@avg,@count,@max,@min
平均值,计数,最大值,最小值
聚合运算符可对数组或一组属性进行操作,从而生成一个反映集合某些方面的单个值。
NSMutableArray *pArray = [NSMutableArray arrayWithCapacity:10];
...
[pArray valueForKeyPath:@"@sum.age"]
2.数组运算符
返回操作对象指定属性的集合
数组中包含nil,会引发异常
@unionOfObjects
返回数组
@distinctUnionOfObjects
返回数组,去重
//返回的数组里的值,为age
[pArray valueForKeyPath:@"@unionOfObjects.age"]
3.嵌套运算符
嵌套运算符对嵌套集合进行操作,其中集合本身的每个条目都包含一个集合。
@distinctUnionOfArrays
返回数组,去重
@unionOfArrays
,返回数组
@distinctUnionOfSets
返回集合,去重
- 访问非对象属性
KVC默认实现了非对象属性和对象属性之间的转换,
取值时如果返回不是对象,则getter使用该值初始化一个NSNumber对象(用于标量)或NSValue对象(用于结构),然后返回该值。
类似地,默认情况setValue:forKey:下,给定特定key,如果数据类型不是对象,则设置器首先将适当的<type> Value消息发送到传入值对象以提取基础数据,然后存储该数据。
自定义结构体类型:
typedef struct{
double longitude;
double latitude;
}Coordinates;
设置值
Person *p = [[Person alloc]init];
Coordinates coor = {121,17};
NSValue *value = [NSValue valueWithBytes:&coor objCType:@encode(Coordinates)];
[p setValue:value forKey:@"coordinates"];
读取值
NSValue *result = [p valueForKey:@"coordinates"];
Coordinates rCoor;
[result getValue:&rCoor];
-
nil
值的设置
对象类型,设置nil值仍然可用
[p setValue:nil forKey:@"age"];
NSLog(@"%@",[p valueForKey:@"age"]);
kvcDemo[4470:510628] (null)
非对象类型设置nil
- 注意:非对象类型通过
setValue:
设置nil值, 会执行setNilValueForKey:
。 此方法的默认实现引发NSInvalidArgumentException异常,但是子类可以覆盖此行为
运行时会报错
[p setValue:nil forKey:@"coordinates"];
搜索模式
获取值
valueForKey:
根据传入的key
从调用它的类内部执行以下过程
找访问器方法:
get<Key>,<key>,is<Key>
,或者_<key>
如果找到了,看第五步上一步没有找到,接下来搜索
countOf<Key>
和objectIn<Key>AtIndex:,objectsAtIndexes:
(看是不是数组)
(1) 如果找到其中的第一个,再找到其他两个中的至少一个,则创建一个响应所有NSArray方法的集合代理对象并将其返回。否则,请继续执行步骤3。
(2) 代理对象随后将任何NSArray接收到的一些组合的消息countOf<Key>,objectIn<Key>AtIndex:
和<key>AtIndexes:
消息给键-值编码创建它兼容的对象。如果原始对象还实现了名称为的可选方法get<Key>:range:,则代理对象也会在适当时使用该方法。实际上,代理对象与与键值编码兼容的对象一起使用,使基础属性的行为就好像它是NSArray,即使不是。上一步没有找到,接下来找
countOf<Key>,enumeratorOf<Key>和memberOf<Key>:
(看是不是集合)
(1) 如果找到所有三个方法,请创建一个响应所有NSSet方法的集合代理对象并返回该对象。随后,此代理对象将NSSet接收到的任何消息转换为,和消息的某种组合countOf<Key>
,以创建该对象的对象。实际上,代理对象与与键值编码兼容的对象一起使用,使基础属性的行为就好像它是,即使不是。enumeratorOf<Key>memberOf<Key>:NSSet
。
(2) 没找到,来到第4步如果发现收集的访问方法没有简单的存取方法或者组,如果接收器的类方法
accessInstanceVariablesDirectly
返回YES
,搜索名为实例变量_<key>
,_is<Key>
,<key>
,或者is<Key>
,按照这个顺序。如果找到,请直接获取实例变量的值,然后继续执行步骤5。否则,请继续执行步骤找到对应的属性
(1) 如果找到是个对象,直接返回;
(2) 如果该值是支持NSNumber
支持的类型,则将其存储在NSNumber实例中并返回它;
(3) 如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象;如果以上都没找到,会调用
- (id)valueForUndefinedKey:(NSString *)key
,该方法默认引发异常;
设置值
setValue:forKey:根据
key` 按照以下步骤查找
查找第一个名为set<Key>:or的访问器_set<Key>,过访问器完成设置;
如果没有找到简单的访问,如果类方法
accessInstanceVariablesDirectly
返回YES
,寻找一个实例变量与名称类似_<key>,_is<Key>,<key>
,或者is<Key>
,按照这个顺序。如果找到,直接用输入值(或展开值)设置变量并完成操作;
3.在找不到访问器或实例变量后,调用setValue:forUndefinedKey:。默认情况下会引发异常,但是的子类可以提供特定行为;
验证属性
validateValue:forKey:error:
键值编码协议定义了支持属性验证的方法,可以重写
验证方法认为值对象有效,并在YES不更改值或错误的情况下返回。
验证方法认为值对象无效,但选择不对其进行更改。在这种情况下,该方法返回NO错误参考并将错误参考(如果由调用者提供)设置到一个NSError指示失败原因的对象。
验证方法认为值对象无效,但创建了一个新的有效对象作为替换。在这种情况下,该方法返回,YES而错误对象保持不变。在返回之前,该方法将值引用修改为指向新值对象。进行修改时,即使值对象是可变的,该方法也总是创建一个新对象,而不是修改旧对象。
Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
NSLog(@"%@",error);
}
最后:
键值编码是高效的,尤其是当您依靠默认实现来完成大部分工作时,但是它确实增加了一个间接级别,该级别比直接方法调用要慢一些。因为,KVC需要解析字符串来计算你需要的的答案,此外编译器还无法对它进行错误检查,像键路径,编译器无法判断它是否是错误的路径。
仅当您可以从键值编码中受益时才使用键值编码,或者仅让您的对象参与依赖它的Cocoa技术时,才使用键值编码。