KVC 的实现

1.KVC是啥?

键值编码(key value coding)是一种可以通过字符串的名字(key)来访问类属性的机制。区别于通过调用Setter、Getter来访问属性的方法。

2.啥用?

通常我们要访问一个类中属性的值时,可以使用点语法(如 people.name)。但当你想访问私有属性或成员变量时,点语法就没用了。而有了 KVC 问题就迎刃而解。

  • 常用方法?

NSKeyValueCoding.h中有个 NSObject 的分类:NSObject(NSKeyValueCoding),其中定义了以下方法:

1
2
3
4
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

其中,-valueForKey: 与 -valueForKeyPath:在一般的属性访问时,效果是一样的。但要访问类似下面案例1中 Student.index 这种子属性时,就只能使用后者,使用前者编译时没问题但运行时会崩溃。

#示例1:

1
2
3
4
5
6
7
8
9
10
@interface Student : NSObject
@property (nonatomic,assign) int index;
@end

@interface People : NSObject
{
NSString *name;//私有变量
Student *student;
}
@end
1
2
3
4
5
6
7
8
9
10
11
#import "People.h"

@implementation Student
@end

@interface People()
@property (nonatomic, copy) NSString *eMail;//私有属性
@end

@implementation People
@end

调用示例:

1
2
3
4
5
6
7
8
9
10
11
People *people = [People new];
Student *student = [Student new];

[people setValue:@"David" forKey:@"name"];
[people setValue:@"email@" forKey:@"eMail"];
[people setValue:student forKey:@"student"];
[people setValue:@(101) forKeyPath:@"student.index"];

NSString *name = [people valueForKey:@"name"];
int index = [[people valueForKeyPath:@"student.index"] intValue];
NSLog(@"++++name:%@,index:%d",name,index);

3.底层的原理?

NSKeyValueCoding.hKVC的实现过程有详细的解释。

3.1.valueForKey:

  1. 在方法接收者的类中先按照 getKey,key,isKey 的顺序查找 getter 方法,找到直接调用。如果是 BOOL,int 等内建值类型,会做 NSNumber 类型转化。
  2. 没有找到的话,如果方法接收者的 accessInstanceVariablesDirectly 属性返回YES(默认返回YES),那么依次搜索符合_key,_isKey,key,isKey 格式的成员变量,找到后返回它的值。
  3. 再没找到的话,会调用 -valueForUndefinedKey,在没被重写的情况下,此方法默认抛出 NSUndefinedKeyException 异常。

3.2.setValue:forKey:

  1. 首先在方法接收者所属的类中搜索 setKey: 格式的方法并检测其参数类型。如果参数类型符合则直接调用该方法;如果参数不是对象指针类型但值为nil,则会调用 -setNilValueForKey: 并抛出异常;
  2. 第一步中没有找到格式相符的方法的话,如果 accessInstanceVariablesDirectly 属性返回 YES。那么就去依次查询符合_key,_isKey,key,isKey 格式的成员变量,找到后给它赋值。
  3. 如果仍没找到符合的成员变量,则调用 setValue:forUnderfinedKey: 并抛出 NSUndefinedKeyException 异常。

由上述过程可以得知:valueForKey会调用属性的gettersetValue:forKey:会调用属性的setter函数。

#示例2:

1
2
3
4
5
6
7
8
@interface Model : NSObject
@property (nonatomic, strong) NSString *_modelString;
@end

@interface People : NSObject
@property (nonatomic, strong) NSString *stringA;
@property (nonatomic, strong) Model *modelA;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#import "People.h"

@implementation Model
- (void)set_modelString:(NSString *)_modelString
{
__modelString = _modelString;
NSLog(@"++++执行 setter _modelString");
}
- (void)setModelString:(NSString *)modelString
{
NSLog(@"++++执行 setter modelString");
}
- (void)setNoExist1:(NSString *)noExist
{
NSLog(@"++++执行 setter noExist1 ");
}
@end

@implementation People

- (void)setStringA:(NSString *)stringA
{
_stringA = stringA;
NSLog(@"++++执行 setter stringA");
}
- (instancetype)init
{
if (self = [super init]) {
self.modelA = [[Model alloc] init];
}
return self;
}
@end

调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (void)kvcTest
{
People *apeo = [[People alloc] init];
//调用了setter
apeo.stringA = @"stringA setter";
//也调用了setter
①[apeo setValue:@"stringA KVC" forKey:@"stringA"];
//没调用setter 但最终赋值给了此属性对应的成员变量_stringA
②[apeo setValue:@"_stringA KVC" forKey:@"_stringA"];

NSLog(@"++++apeo.stringA 值: %@", apeo.stringA);
NSLog(@"++++++++++++++++++++++++++++++");

// 调用-set_modelString:
③[apeo setValue:@"_modelString kvc" forKeyPath:@"modelA._modelString"];
//不存在的属性,但会调用-setModelString:函数
④[apeo setValue:@"modelString kvc" forKeyPath:@"modelA.modelString"];
//没调用setter 但最终赋值给了modelA的属性对应的成员变量_modelString
⑤[apeo setValue:@"__modelString kvc" forKeyPath:@"modelA.__modelString"];

//不存在的属性 但会调用其setter函数-setNoExist1:
⑥[apeo setValue:@"noExist1" forKeyPath:@"modelA.noExist1"];

NSLog(@"++++apeo.modelA._modelString 值: %@", apeo.modelA._modelString);
NSLog(@"++++++++++++++++++++++++++++++");

NSString *s1 = [apeo valueForKeyPath:@"modelA._modelString"];
NSString *s2 = [apeo valueForKeyPath:@"modelA.modelString"];
NSString *s3 = [apeo valueForKeyPath:@"modelA.__modelString"];
NSLog(@"++++s1:%@ s2:%@ s3:%@",s1,s2,s3);
}

输出日志:

1
2
3
4
5
6
7
8
9
10
++++执行 setter stringA
++++执行 setter stringA
++++apeo.stringA 值: _stringA KVC
++++++++++++++++++++++++++++++
++++执行 setter _modelString
++++执行 setter modelString
++++执行 setter noExist1
++++apeo.modelA._modelString 值: __modelString kvc
++++++++++++++++++++++++++++++
++++s1:__modelString kvc s2:__modelString kvc s3:__modelString kvc

日志显示:①~⑨全部执行成功;其中①③④⑥ 执行了setter方法,⑦⑧执行了getter方法,②⑤⑨直接访问的实例变量。

3.3.小结

当我们使用id objectA = objectB.value2时是否代表 objectB 有一个 value2属性呢?实际上不一定,例如object.class,NSObject 中 并没有class属性,只有一个class方法。

OC的点语法中,.表示调用方法,即.操作只是去寻找一个名称匹配参数匹配的方法。我们习以为常的属性调用只是因为属性刚好有gettersetter方法符合要求而已。如果.表达式在=左边,则该属性的setter方法被调用;如果.表达式在=的右边,则属性的getter方法被调用。

#KVC与集合运算符

多数情况下,keyPath被用来读取对象中子对象的某个属性,如示例1中的[people valueForKeyPath:@"student.index"]。除此之外,苹果还将此方法用在了集合中,用以实现某些常见的集合运算,如求最大值、最小值、求和等,这就是我们要介绍的集合运算符KVC中的应用。

When you send a key-value coding compliant object the valueForKeyPath: message, you can embed a collection operator in the key path. A collection operator is one of a small list of keywords preceded by an at sign (@) that specifies an operation that the getter should perform to manipulate the data in some way before returning it. The default implementation of valueForKeyPath: provided by NSObject implements this behavior.

我们可以在keyPath中加入集合运算符,它们以@开头,用来指定对数据的某种操作,最终返回处理后的结果。具体格式为:

keypath

格式说明:

  • 左边:将要执行运算的集合;
  • 中间:运算符;
  • 右边:集合中对象的属性;

如果是数组调用了valueForKeyPath,则左边部分可以省略;

运算符为数组的count时右边的部分可以忽略,其他运算的右边不能为空。

#示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//银行卡
@interface Card : NSObject
@property (nonatomic) int cardNumber; // 编号
@property (nonatomic) float money; // 余额
@end

//用户
#import "Card.h"
@interface User : NSObject
@property (nonatomic) NSInteger age; //年龄
@property (nonatomic) NSArray *cardArr; //拥有的银行卡
@end

//调用
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
Card *card1 = [Card new];
card1.cardNumber = 100;
card1.money = 1000;

Card *card2 = [Card new];
card2.cardNumber = 101;
card2.money = 2000;

User *user = [User new];
user.age = 20;
user.cardArr = @[card1,card2];

User *user2 = [User new];
user2.age = 30;

NSArray *userArr = @[user,user2];

int min = [[user valueForKeyPath:@"cardArr.@min.money"] intValue];
int max = [[user valueForKeyPath:@"cardArr.@max.money"] intValue];
int sum = [[user valueForKeyPath:@"cardArr.@sum.money"] intValue];
int avg = [[user valueForKeyPath:@"cardArr.@avg.money"] intValue];
int count = [[userArr valueForKeyPath:@"cardArr.@count"] intValue];
//直接对数组进行查询
int count2 = [[userArr valueForKeyPath:@"@count"] intValue];
int maxAge = [[userArr valueForKeyPath:@"@max.age"] intValue];

return YES;
}

#示例2:数组中数字运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)kvcToSumArr
{
NSArray *array = @[@"1",@"2", @"3"];
// 最大值
int max = [[array valueForKeyPath:@"@max.intValue"] intValue];
// 最小值
int min = [[array valueForKeyPath:@"@min.intValue"] intValue];
// 求和
int sum = [[array valueForKeyPath:@"@sum.intValue"] intValue];
// 平均值
float avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];

NSLog(@"+++Max:%d,Min:%d,Sum:%d,Avg:%f",max,min,sum,avg);
//输出+++Max:3,Min:1,Sum:6,Avg:2.000000
}

相关参考:

#©Apple-KVC Using Collection Operators

#©掘金


KVC 的实现
https://davidlii.cn/2018/04/12/kvc.html
作者
Davidli
发布于
2018年4月12日
许可协议