复习一下KVC
一. 前言
kvc(key value coding)是cocoa框架为开发者提供的非常强大的工具,简单解释为:键值编码。它依赖于runtime,在oc的动态性方面发挥了重要作用。
它主要的功能在于直接通过变量名称字符串来访问成员变量,不管是私有的还是共有的,这也是为什么对于oc来说没有真正的私有变量,因为它们都可以使用kvc访问。
二. 使用场景
下面是kvc的一些实用场景,读者可自行编码尝试。
1.访问私有属性
例如设置uitextfield的placeholder颜色,常规的方法是:
// 方式一:常规设置 _nametextfield.attributedplaceholder = [[nsattributedstring alloc] initwithstring:@"请输入名字" attributes:@{nsforegroundcolorattributename : [uicolor redcolor]}];
通过kvc的方式设置:
// 方式二:使用kvc获取私有属性 _nametextfield.placeholder = @"请输入名字"; // uilabel *placeholderlabel = [_nametextfield valueforkey:@"placeholderlabel"]; uilabel *placeholderlabel = [_nametextfield valueforkey:@"_placeholderlabel"]; placeholderlabel.textcolor = uicolor.blackcolor; [self.view addsubview:_nametextfield];
这里通过valueforkey的方式获取了uitextfield的placeholderlabel,然后对该对象的颜色进行设置。如果获取到的placeholderlabel为nil,有几种可能:
- key输入有误(重新输入)
- 系统实现发生改变
- 苹果使用的lazy load,导致在使用valueforkey获取的时候还没有初始化(因此这里先赋值placeholder,然后再获取placeholderlabel)
还是建议使用使用方式一对属性进行设置。站在苹果的角度,它之所以把某些属性设置为私有,就是不想让开发者进行直接修改,后续一旦苹果对系统实现有所更改,那就会导致使用kvc获取的内容失效。
另外,在使用setvalue:forkey:的时候一定要类型统一,比如你通过key获取到的是一个label,却将string设置为了value,将会crash。
上面在使用valueforkey:方法的时候参数可以带下划线( _ placeholderlabel ),也可以不带下划线,它的主要区别就是如果使用了带下划线的key,就算类中手动实现了getter方法,也不会执行类中实现的getter方法。如果使用了不带下划线的,将会执行类中getter方法。setter也是如此。
2.对象关系映射
在没有比较成熟的第三方model解析(如mantle)前,orm(object relational mapping)可以使用kvc进行处理:
- (instancetype)init { return [self initwithjsondictionary:nil]; } - (instancetype)initwithjsondictionary:(nsdictionary *)andictionary { if (self = [super init]) { [self setvaluesforkeyswithdictionary:andictionary]; } return self; } - (void)setvalue:(id)value forundefinedkey:(nsstring *)key { nslog(@"%@",key); } // - (void)objectrelationalmapping { nsdictionary *personinfodictionary = @{ @"name" : @"zhangsan", @"age" : @"30", @"school" : @"hist" }; person *p1 = [[person alloc] initwithjsondictionary:personinfodictionary]; nslog(@"%@",p1); } @end
这里主要使用了setvaluesforkeyswithdictionary:方法。需要注意的是,需要实现setvalue:forundefinedkey:方法,因为当字典中包含的key在person属性中并不一定存在,如果不存在的话,就会调用setvalue:forundefinedkey:。该方法默认抛出nsundefinedkeyexception异常。所以需要对其进行重写,避免crash。
3. 使用keypath实现多级访问
kvc除了setvalue:forkey:方法,还有setvalue:forkeypath:方法。具体使用如下:
_nametextfield.placeholder = @"请输入名字"; [_nametextfield setvalue:uicolor.blackcolor forkeypath:@"placeholderlabel.textcolor"];
这里一步操作,就完成了对placeholder颜色的变更。
4. 安全性访问
现在person有一个friends方法,属性声明如下:
@property (nonatomic, copy) nsmutablearray *friends;
此时可以通过如下的方式进行设置friends:
nsarray *personsarray = ...; person *zhangsan = [[person alloc] init]; zhangsan.name = @"zhangsan"; [[zhangsan mutablearrayvalueforkey:@"friends"] addobjectsfromarray:personsarray];
这样就可以顺利将personsarray赋值给zhangsan的friends。接下来换个操作:将属性改为
@property (nonatomic, copy) nsarray *friends;
其他代码保持不变。再次执行,依然会给zhangsan.friends赋值。而且没有任何crash或者异常。由此可见,通过mutablearrayvalueforkey这种方式进行处理,可以对于不可变的集合类型,提供安全的可变访问,即使是不可变数组,也可以增加数组元素。
5. 函数操作
使用kvc,可以很方便地进行一些基本的函数操作,例如:
nsmutablearray *personsarray = [[nsmutablearray alloc] initwithcapacity:5]; for (nsinteger i = 0; i < 5; i ++) { nsstring *tempname = [nsstring stringwithformat:@"people%ld",(long)i]; nsdictionary *personinfodictionary = @{ @"name" : tempname, @"age" : @(10 + i), @"school" : @"hist" }; person *tempperson = [[person alloc] initwithjsondictionary:personinfodictionary]; [personsarray addobject:tempperson]; } nsnumber *count = [personsarray valueforkeypath:@"@count"]; nsnumber *sumage = [personsarray valueforkeypath:@"@sum.age"]; nsnumber *avgage = [personsarray valueforkeypath:@"@avg.age"]; nsnumber *maxage = [personsarray valueforkeypath:@"@max.age"]; nsnumber *minage = [personsarray valueforkeypath:@"@min.age"];
其中@表示是数组特有的键,而不是名为count的键。可以使用valueforkeypath:快速进行计算。还可以进行更复杂的一些计算:
nsarray *array = @[@"apple", @"banner", @"apple", @"orange"]; nslog(@"%@", [array valueforkeypath:@"@distinctunionofobjects.self"]); // orange,apple,banner nsarray *array1 = @[@[@"apple", @"banner"], @[@"apple", @"orange"]]; nslog(@"%@", [array1 valueforkeypath:@"@unionofarrays.self"]); // apple,banner,apple,orange nsmutablearray *personsarray1 = [[nsmutablearray alloc] initwithcapacity:5]; nsmutablearray *personsarray2 = [[nsmutablearray alloc] initwithcapacity:5]; for (nsinteger i = 0; i < 5; i ++) { nsstring *tempname = [nsstring stringwithformat:@"people%ld",(long)i]; nsdictionary *personinfodictionary = @{ @"name" : tempname, @"age" : @(10 + i), @"school" : @"hist" }; person *tempperson = [[person alloc] initwithjsondictionary:personinfodictionary]; i % 2 == 0 ? [personsarray1 addobject:tempperson] : [personsarray2 addobject:tempperson]; } nsarray *personsarray = @[personsarray1, personsarray2]; nslog(@"%@",[[personsarray valueforkeypath:@"@unionofarrays.age"] valueforkeypath:@"@sum.self"]); // 60
类似上面的代码,可以使用kvc对复杂的操作进行简单化,而没有必要再使用for循环或者其他遍历操作。
三. kvc验证
person *person = [[person alloc] init]; [person setvalue:[uicolor redcolor] forkey:@"name"];
name是string类型,但是传入一个uicolor类型也没有异常或者crash产生,这也是一个潜在的问题,一旦按照string使用name,就会出现问题。因此需要对value类型与key是否匹配进行判断。kvc提供了如下的方法:
person *person = [[person alloc] init]; uicolor *color = [uicolor redcolor]; nserror *error = nil; bool isvalidate = [person validatevalue:&color forkey:@"name" error:&error]; if (isvalidate && !error) { [person setvalue:color forkey:@"name"]; }
发现依然可以设置成功,validatevalue:forkey:error:方法竟然返回了yes。根据官方文档可知,此方法的默认实现将搜索接收方的类,寻找名称匹配validate
- (bool)validatename:(id *)iovalue error:(nserror **)error {
if ([*iovalue iskindofclass:[nsstring class]]) {
return yes;
}
return no;
}