第六部分,KVC。

键值编码(Key-Value Coding),或者说 NSKeyValueCoding 是一个非正式协议,NSObject 中是默认实现这个协议的。KVC 允许我们用属性的字符串名称来访问属性,字符串在这儿叫做键(Key),不是直接调用对象的 Accessor 方法或是直接访问成员对象。

Key & Key Path

键 Key

KVC 简单的访问和设置属性值。

1
2
3
4
5
6
7
8
// 属性
@property (nonatomic, copy) NSString *name;

// 取值
NSString *n = [object valueForKey:@"name"]

// 设值
[object setValue:@"Evan" forKey:@"name"]

键路径 Key Path

KVC 同样允许我们通过关系来访问对象。假设 person 对象有属性 address,address 有属性 city,我们可以这样通过 person 来访问 city:

1
[person valueForKeyPath:@"address.city"];

KVC 如何寻找 Key?

简单设值

setValue:forKey:

  1. 程序首先调用 set<Key>: 或者 _set<Key> 设置方法。
  2. 如果上述方法没有找到,KVC 机制会检查 + (BOOL)accessInstanceVariablesDirectly 方法是否返回 YES。该方法默认返回 YES,如果重写设置为 NO,那么在这一步就会执行 - (void)setValue:forUndefinedKey: 方法,默认是抛出异常,不推荐设置为 NO
  3. + (BOOL)accessInstanceVariablesDirectly 设置为 YES 前提下,Foundation 会按照 _<key>_is<Key><key>is<Key> 的顺序在类接口定义和实现处查找实例变量,再赋值。
  4. 如果上述方法和实例变量都不存在,就执行 - (void)setValue:forUndefinedKey:

简单取值

valueForKey:

  1. 程序按照 get<Key><key>is<Key>_<key> 的顺序查找获取方法,找到直接调用。如果是 BOOL 或者 Int 等值类型,会将其包装成一个 NSNumber 对象。
  2. 如果上面的 getter 没有找到,KVC 则会查找 countOf<Key>objectIn<Key>AtIndex<Key>AtIndexes 格式的方法。如果 countOf<Key> 方法和另外两个方法中的一个被找到,那么就会返回一个可以响应 NSArray 所有方法的代理集合(它是 NSKeyValueArray,是 NSArray 的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于 NSArray 的方法,就会以countOf<Key>objectIn<Key>AtIndex<Key>AtIndexes 这几个方法组合的形式调用。还有一个可选的 get<Key>:range: 方法。
  3. 如果上面的方法没有找到,那么会同时查找 countOf<Key>enumeratorOf<Key>memberOf<Key> 格式的方法。如果这三个方法都找到,那么就返回一个可以响应 NSSet 所的方法的代理集合,和上面一样,给这个代理集合发 NSSet 的消息,就会以 countOf<Key>enumeratorOf<Key>memberOf<Key> 组合的形式调用。
  4. 如果还没有找到,在 + (BOOL)accessInstanceVariablesDirectly 设置为 YES 前提下,Foundation 会按照 _<key>_is<Key><key>is<Key> 的顺序在类接口定义和实现处查找实例变量。
  5. 还未找到,就执行 - (id)valueForUndefinedKey:

集合运算符

KVC 集合运算符允许在 valueForKeyPath: 方法中使用 Key Path 符号在一个集合中执行方法。无论什么时候,你在 Key Path 中看见 @,它都代表了一个特定的集合方法,其结果可以被返回或者链接,就想其他 Key Path 一样。

三种类型:

  • 简单集合运算符:返回的是 string,number,或者 date
  • 对象运算符:返回的是一个数组
  • 数组和集合运算符:返回的是一个数组或者集合

KVC 会在必要的时候,把基本数据类型的数据自动装箱和拆箱到 NSNumber 或者 NSValue 中来确保一切工作正常。

简单集合运算符

  • @count:返回一个值为集合中对象总数的 NSNumber 对象
  • @sum:首先把集合中的对象都转换为 double 类型,然后计算其总和,最后返回一个值为总和的 NSNumber 对象
  • @avg:把集合中的每个对象都转换为 double 类型,返回一个值为平均值的 NSNumber 对象
  • @max:使用 compare: 方法来确定最大值。为了让其正常工作,集合中所有的对象都必须支持和另一个对象的比较
  • @min:和 @max 一样,但是返回的是集合中的最小值
1
2
3
4
5
[products valueForKeyPath:@"@count"]; // 4
[products valueForKeyPath:@"@sum.price"]; // 3526.00
[products valueForKeyPath:@"@avg.price"]; // 881.50
[products valueForKeyPath:@"@max.price"]; // 1699.00
[products valueForKeyPath:@"@min.launchedOn"]; // June 11, 2012

提示:
你可以简单的通过把 self 作为操作符后面的 Key Path 来获取一个由 NSNumber 组成的数组或者集合的总值。

1
[@[@(1), @(2), @(3)] valueForKeyPath:@"@max.self"];

对象操作符

  • @unionOfObjects:返回一个由操作符右边的 Key Path 所指定的对象属性组成的数组,不去重
  • @distinctUnionOfObjects:会对返回的数组,去重
1
2
3
4
5
6
7
NSArray *inventory = @[iPhone5, iPhone5, iPhone5, iPadMini, macBookPro, macBookPro];

[inventory valueForKeyPath:@"@unionOfObjects.name"];
// "iPhone 5", "iPhone 5", "iPhone 5", "iPad Mini", "MacBook Pro", "MacBook Pro"

[inventory valueForKeyPath:@"@distinctUnionOfObjects.name"];
// "iPhone 5", "iPad Mini", "MacBook Pro"

数组和集合操作符

  • @unionOfArrays / @distinctUnionOfArrays:返回了一个数组,其中包含这个集合中每个数组对于这个操作符右边指定的 Key Path 进行操作之后的值。带 distinct 的会去重
  • @distinctUnionOfSets:它期望的是包含着 NSSet 对象的 NSSet,并且会返回一个 NSSet 对象。因为集合不能包含重复的值,所以他只有 distinct 操作。
1
2
[@[appleStoreInventory, verizonStoreInventory] valueForKeyPath:@"@distinctUnionOfArrays.name"]; 
// "iPhone 5", "iPad Mini", "MacBook Pro"

KVV

键值验证(Key-Value Validate),也是 KVC API 的一部分,这是一个用来验证属性值的 API。但是,KVC 不会做任何的验证,也不会调用任何 KVV 的方法,这是开发者自己需要做的事儿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)testKVV
{
Person *aPerson = [[Person alloc] init];
NSString *name = @"Evan";
NSString *key = @"name";
NSError *error = nil;
// KVV 验证
BOOL isLegalName = [aPerson validateValue:&name forKey:key error:&error];
if (isLegalName) {
NSLog(@"it's a legal name.");
[aPerson setValue:name forKey:key];
} else {
NSLog(@"the name is illegal.");
}
}
1
2
3
4
5
6
7
8
9
10
@implementation Person
- (BOOL)validateName:(inout NSString * _Nullable __autoreleasing *)name error:(out NSError * _Nullable __autoreleasing *)outError
{
if ((*name).length == 0) {
(*name) = @"default";
return NO;
}
return YES;
}
@end

Reference