参考官方文档
KVC概述
键值编码是一种由NSKeyValueCoding非正式协议启用的机制,对象采用该机制提供对其属性的间接访问。键值编码是一个基本概念,是许多其他Cocoa技术的基础,在某些情况下,键值编码还有助于简化代码。
KVC可以通过键值的方式对对象的属性进行存取操作,这使得在运行时操作对象的属性成为可能。
我们通常都是使用getter
和setter
方法去访问和设置属性的值,有了KVC我们可以直接访问属性的基础实例变量,当然这样可读性和性能并不是很高,一般不推荐这么去用。
KVC的使用大全
Part1、普通用法
1、取值valueForKey:
普通对象
[stu valueForKey:@"age"];
数组等集合对象的处理
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
2、赋值setValue:forKey:
[stu setValue:@(18) forKey:@"age"];
3、批量处理
给定字典可以根据keys的数组返回所有符合的键值对,以字典的形式返回
dictionaryWithValuesForKeys
,setValuesForKeysWithDictionary
.
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int length;
@property (nonatomic, strong) NSMutableArray *penArr;
@property (nonatomic, strong) Student *stu;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [Person new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(180 + 2*arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
}
@end
Part2、keyPath用法
1、将数组中所有对象的name属性值取出,并放入一个数组中返回
NSArray *names = [array valueForKeyPath:@"name"];
同理,也可以获取每个数组元素的length
属性
- (void)arrayMessagePass{
NSArray *array = @[@"First",@"Second",@"Third",@"Forth"];
NSArray *lenStr= [array valueForKeyPath:@"length"];
NSLog(@"lenStr = %@",lenStr);
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"lowStr = %@",lowStr);
}
打印如下
2、在keyPath里,key也可以使用点语法。
@interface Student : NSObject
{
@public
NSArray *books;
NSString *name;
int age;
}
@property (nonatomic, copy) NSString *sex;
@end
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int length;
@property (nonatomic, strong) NSMutableArray *penArr;
@property (nonatomic, strong) Student *stu;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [Person new];
NSLog(@"%@", [p valueForKeyPath:@"stu.sex"]);
}
@end
容错处理
当KVC经过搜索模式没有获取到对应的值时,会调用相对应的异常方法,导致应用程序Crash,常见的异常有三种,为了防止出现这种严重的错误,我们通常的做法是重写相对应的异常方法,如下所示
1、'NSUnknownKeyException', reason: '[<Person 0x6000016f1e00> valueForUndefinedKey:]: this class is not key value coding-compliant for the key hahaName.'
- (void)setNilValueForKey:(NSString *)key{
NSLog(@"%@的值不能为空",key);
}
2、'NSUnknownKeyException', reason: '[<Person 0x6000008ffe80> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xixiName.'
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"不能对不存在的健赋值");
}
3、NSInvalidArgumentException', reason: '[<Person 0x60000295ca00> setNilValueForKey]:
- (id)valueForUndefinedKey:(NSString *)key{
NSLog(@"不能对不存在的键取值");
return @"error";
}
KVC集合类运算
使用 KVC 进行集合类的运算,例如求一个数组中所有Person对象的length总和。
如图所示,
-
keyPathToCollection
:Left key path,要操作的集合对象,若调用 valueForKeyPath: 方法的对象本来就是集合对象,则可以省略; -
collentionOperator
:Collection operator,集合操作符,一般以@开头; -
keyPathToproperty
:Right key path,要运算的属性。
集合运算符分为三种:集合操作符(返回NSNumber)、数组操作符(返回数组)、嵌套操作符
Part1、集合操作符(Aggregation Operators)
返回的是NSNumber类型的值,有以下5种用法,分别是平均、总和、数量、最大、最小。
- 1、@avg用来计算指定属性的平均值,返回NSNumber
- 2、@sum属性总和,返回NSNumber
- 3、@count用来计算集合里对象的数量
- 4、@max属性最大值
- 5、@min属性最小值
用法如下所示
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
Person *p = [Person new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(180 + 2*arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
[personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"length"]);
float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
NSLog(@"集合中有length属性的元素,length属性的平均值为 = %f", avg);
int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
NSLog(@"集合中有length属性的元素的数量为 = %d", count);
int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
NSLog(@"集合中有length属性的元素,length属性的总和为 = %d", sum);
int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
NSLog(@"集合中有length属性的元素,length属性的最大值为 = %d", max);
int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
NSLog(@"集合中有length属性的元素,length属性的最小值为 = %d", min);
打印结果如下
max和min是通过compare:方法进行比对的,所以需要属性满足能通过compare方法比对
Part2、数组操作符(Array Operators)
与集合操作符的区别是,数组操作符返回值的是数组。它有以下2种用法
- 1、@unionOfObjects返回指定属性的数组
- 2、@distinctUnionOfObjects返回指定属性去重后的集合,即数组中payee属性值重复的都去掉(去重)。
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
Person *p = [Person new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
[personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"length"]);
// 返回操作对象指定属性的集合
NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.length"];
NSLog(@"arr1 = %@", arr1);
// 返回操作对象指定属性的集合 -- 去重
NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.length"];
NSLog(@"arr2 = %@", arr2);
打印结果如下:
从结果中我们知道,
@unionOfObjects
的作用是返回数组元素中符合length
属性的元素;而@distinctUnionOfObjects
在此基础上做了一步去重操作。Part3、嵌套数组
嵌套运算符在嵌套集合上运行,集合的每个元素本身都包含一个集合。它有以下2种用法
- 1、@unionOfArrays:valueForKeyPath创建并返回一个数组,该数组包含和属性对应的集合的所有对象,且不删除重复项
- 2、@distinctUnionOfArrays:valueForKeyPath创建并返回一个数组,该数组包含和属性对应的所有集合的组合的不同对象,就是去重。
NSMutableArray *personArray1 = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGPerson *person = [LGPerson new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[person setValuesForKeysWithDictionary:dict];
[personArray1 addObject:person];
}
NSMutableArray *personArray2 = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGPerson *person = [LGPerson new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[person setValuesForKeysWithDictionary:dict];
[personArray2 addObject:person];
}
// 嵌套数组
NSArray* nestArr = @[personArray1, personArray2];
NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];
NSLog(@"unionOfArrays = %@", arr1);
NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];
NSLog(@"distinctUnionOfArrays = %@", arr);
打印结果如下:
从结果中我们可以得知,@unionOfArrays
的作用是将所有嵌套的集合,通过属性全部整合到一个集合中了;而@distinctUnionOfArrays
在此基础上又做了一步去重操作。
KVC的搜索模式
Part1、基于Getter的搜索模式
- 1、先按顺序查找访问方法get<Key>,<key>,is<Key>,或者_<key>。
- 2、如果getter方法没有找到,则尝试寻找countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndexes,如果找到了第一个和剩下两个中的一个方法,就会返回一个数组
- 3、如果还是没有找到,则尝试countOf<Key>,enumeratorOf<Key>和memberOf<Key>:,如果上述三个方法都找到了,就去创建一个NSSet类型的对象,则会返回一个可以响应NSSet所有方法的对象。
- 4、如果上述方式没有找到,判断accessInstanceVariablesDirectly的值为YES时,在内存中搜索_<key>,_is<Key>,<key>,或者is<Key>,找到了就执行5,没找到就执行步骤6.
- 5、返回结果
- 6、如果都失败了,调用valueForUndefinedKey:抛出异常。
举个例子看比较好理解一些
@interface Student : NSObject
{
@public
NSArray *books;
NSString *name;
int age;
}
@property (nonatomic, copy) NSString *sex;
@end
@implementation Student
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *stu = [Student new];
stu->name = @"stupid bird";
NSLog(@"name = %@", [stu valueForKey:@"name"]);
//[stu setValue:@(18) forKey:@"age"];
}
@end
从代码中我们知道Student
类包含了一个名为name
的实例变量,它默认是没有实现setter
和getter
方法的,在ViewDidLoad
中给stu
的name
属性赋值,再通过valueForKey:
的方式取值,看看打印的结果是什么?
可以看到,获取到了
name
属性的结果,我们对比上面的Getter的搜索顺序,不难发现它走到了第4步,从内存中获取的name
属性值。
这一次我们在Student类中增加两个方法
@implementation Student
//name****************
//返回name属性的个数
- (NSUInteger)countOfName {
NSLog(@"%s", __func__);
return 5;
}
//返回每个name属性对应的值
- (NSString *)objectInNameAtIndex:(NSUInteger)index {
return [NSString stringWithFormat:@"name %lu", index];
}
@end
打印结果如下:
我们发现获取到的并不是name
属性的结果,对比上面的Getter的搜索顺序,发现它是执行到了第2步,直接返回了一个属性为name
的数组。
以上就是探索基于Getter的搜索模式。
Part2、基于Setter的搜索模式
- 1、按顺序查找名为
set<Key>:
或_set<Key>
方法,如果找到,则使用该方法进行赋值。- 2、如果没有找到,且
accessInstanceVariablesDirectly
返回YES
,则在内存中查找_<key>
,_is<Key>
,<key>
,或者is<Key>
的实例变量,按照这个顺序。如果找到,则直接赋值。- 3、如果还找不到,则调用
setValue:forUndefinedKey:
抛出异常。
以上是官方文档说的Setter赋值时的搜索模式,我们用代码实验一下
@interface Person : NSObject
@end
@implementation Person
#pragma mark - set相关
- (void)setName:(NSString *)name{
NSLog(@"%s",__func__);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s",__func__);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *p = [[LGPerson alloc] init];
[p setValue:@"name" forKey:@"name"];
// NSLog(@"%@",[p valueForKey:@"name"]);
}
@end
按照官方说的搜索步骤,先按顺序查找名为set<Key>:
或_set<Key>
方法,看看打印结果
注释掉
setName:
方法试试
@implementation Person
//#pragma mark - set相关
//- (void)setName:(NSString *)name{
// NSLog(@"%s",__func__);
//}
- (void)_setName:(NSString *)name{
NSLog(@"%s",__func__);
}
@end
打印结果如下
好的,打印结果上看是从
_setName
方法中获取的,官方说的没错(噗)。
为了验证下面步骤,按如下修改Person类
@interface LGPerson : NSObject{
@public
NSString *name;
NSString *_name;
NSString *_isName;
NSString *isName;
}
@end
@implementation Person
@end
在viewDidLoad
中打断点,如下所示
发现
_name
属性被赋值了,所以官方说的对。至此,就不再去质疑官方了,我记住了,在KVC进行Setter方法赋值时,若没有setter方法,会按照
_<key>、_is<key>、<key>、is<key>
的顺序赋值。(偷笑)
写这篇的目的是更好的理解KVC的机制,希望和大家一起进步。