什么是 KVO
KVO(Key Value Coding)是一种非正式协议,它提供了一种间接访问对象属性的方法,也就是通过字符串标识属性。直接访问对象属性的方法就是调用存取方法,或直接使用实例变量。
KVO 是比较基本的技术点,经常与其他技术交互使用。在使用 Cocoa 绑定、KVO、Core Data 时,需要用到 KVC 技术。
存取方法,见名知意,就是用来设置和获得对象数据模型属性值的方法。有两种基本的存取方法,第一种是 getter ,它返回属性的值。第二种是 setter ,它设置属性的值。可能你会感到疑惑,说我并没有见到或者使用这些方法啊,你是因为 Foundation 已经默认为你实现了。
还是举例说明:
@interface People : NSObject
{
NSString *_name; // 实例变量
}
@property (nonatomic, assign) NSUInteger age; // 属性
@end
// 在程序中调用
_name = @"WellCheng";
self.age = 22;
// 上面的代码等同于
[self setAge: 22];
_age = 22;
上面的代码中,直接使用实例变量 _name 并给其赋值。调用 age 属性时,使用了 setter 存取器。有关更多存取器、属性、assign 关键字等内容就不做更多说明了,只要明白 KVC 跟访问器有关就好了。
在程序中使得 KVC 兼容存取器很重要,这样让数据进行了封装又促进了与 Cocoa 绑定、KVO 和 Core Data 的集成,并且还能显著的减少代码。
使用 KVC 简化代码
假如有这样一个需求,在一个方法中,根据参数返回对象不同的实例变量值。
- (id)valueForPeople:(People *)p withParam:(NSString *)identifier {
if [identifier isEqualToString: @"name"] {
return p.name;
}
if [identifier isEqualToString: @"age"] {
return p.age;
}
// ...
}
如果 People 这个类有很多的属性,那么这个方法将会变的很长。下面我们使用 KVC 简化:
- (id)valueForPeople:People(People *)p withParam:(NSString *)identifier {
return [p valueForKey:identifier];
}
KVC 一句话搞定。赞赞哒~
KVO 基础知识
Keys 和 key Paths
key 标识对象的某个属性,通常是存取器的方法名或者属性名。对于 People 类的对象来说,可以是 name、age、birthdayDate 等。
Key Path 是由点分隔的字符串,用来获取更深层次的属性。假若 birthdayDate 是 Date 类型,并且 Date 类还有 year 、month、day 等属性。那么 birthdayDate.day
就是 key path。
通俗点来说,key path 就是为了更加方便的获取更深层级的属性,如果只能获取到对象第一层的属性,那么 KVC 价值就不大了。
如果不使用 keyPath,可能我们的代码会是这样子:
[[self valueForKey:@"birthdayDate"] valueForKey:@"year"];
如果像上面只有两层还好,如果有多层,那这代码也太不优雅了。试想一下,长长的一串 valueForKey --!
使用 KVC 获取属性值
valueForKey:
方法返回指定 key 对应的值。如果对象中没有该 key 对应的存取器方法或者实例变量,对象将调用自身的valueForUndefinedKey
方法,此方法默认的实现为抛出 NSUndefinedKeyException 异常,子类化此方法可覆盖这个默认的行为。
在实际的使用中,我们一般情况下是需要实现这个方法来做一些容错处理的。
dictionaryWithValuesForKeys:
这个方法就比较厉害了,它将返回一个字典,key 仍为传入的 key,key 对应的值为单独调用 valueForKey:
的结果。
如果传入的 key 为 nil ,也会按照 undefined 处理跑出异常,如果有需要在数组中返回 nil,需要用 NSNull 类封装。
如果 key path 返回的值是对应多个对象,那么将会全部返回。
使用 KVC 设置属性值
setValue:forKey:
方法设置指定 key 的值,此方法默认对于 NSValue 封装进行解包,用于处理常量和结构体。
同样,如果 key 不存在,那么将默认发送 setValue:forUndefinedKey:
消息,消息的默认实现也是抛异常。
setValuesForKeysWithDictionary:
方法用于一组 key 的设置。
有一种情况是对于非对象的值设置为 nil,这种情况下将调用自身的 setNilValueForKey:
方法,此方法的默认实现仍然是抛异常,所以如果有这种特殊的需求,需要特殊处理。
这个主要用于当对常量或者非对象的结构体发送了这个方法时,我们将其转换一下,比如对于 Double 类型发送 nil,按照本意就是将 Double 类型的变量置为 0 ,如果是 BOOL 类型的,置为 false 即可,具体情况具体灵活运用。
也许你有传入 key 为 nil 的需求,这个时候,就需要使用 NSNull 类了。KVC 会自动将 [NSNull null] 转换为 nil 进行调用。
点语法与 KVC
可能你会对于 keyPath 中的点语法与 self 的点语法有一些疑惑,其实这两者之间没有什么关系。
keyPath 中的点是用来区分元素边界的,只是当时恰好用点来分割。self 中的点是语法糖,为了方便而已,毕竟写一串大括号还是很丑的。其最终仍然是方法调用。即
self.birthdayDate.year = @"1993";
// 等同于
[[self birthdayDate] year] = @"1993";
// 当然,如果你想要使用 KVC 的方式来简单赋值也并不是不行
// 下面的调用与上面的结果相同
[self setValue: @"1993" ForKeyPath:@"birthdayDate.year"];
KVC 与存取方法
为了让 KVC 能够准确找到存取方法,你需要实现 KVC 对应的存取方法。在对一个类发送了 valueForKey 消息后,KVC 总得能找到对应的实现吧。
常见的存取模式
返回属性值的方法格式为 -<key> ,方法返回一个对象、常量或结构体。-is<key> 用于 Boolean 属性。BOOL 类型在这里是比较特殊的。
另外还有一点需要注意的就是对于非对象类型的属性值,如果被设置为 nil,需要做特殊处理。子类化 setNilValueForKey 方法并做特殊判断即可。
一对多关系(To-Many)中的集合访问器方法
尽管仍然可以通过 -<key> 和 -set<Key>: 的方式处理对多关系的属性,但是这样并不是很高效,因为你在执行操作前需要将集合类型解包出来。所以最好的方式仍然是提供额外的存取器方法。
比如对于 Person 类来说,它的 friendNames 属性是许多个人的名字,属于集合类型,这是一个典型的"一对多"关系。对于它的访问:
- 间接:通过 KVC 获取到集合属性,比如一个 NSArray 的对象,然后对这个对象进行操作
- 直接:实现 Apple 提供的方法模型,以达到访问的目的。
通过实现集合的存取方法,我们可以模拟出一个在类外面看起来是集合的对象。这样我们通过在类的内部实现相关的 KVC 集合方法,类的外面在调用时,根本感觉不到类里面使用 KVC 实现的。
这些思想得用具体的代码实现一下才能体会到 KVC 的特性。
这里有两种差异较大的集合存取器。
有序的集合
有序的集合关系中,存在计总、取回、添加和替换等操作。通常这种关系是 NSArray 或 NSMutableArray 的实例。
Getter
为了支持只读的访问属性:
- -countOf<Key>,必须,类似于 NSArray 的 count 方法。
- -objectIn<Key>AtIndex: 或 -<key>AtIndexes:必须实现,相当于 NSArray 类的 objectAtIndex: 和 objectsAtIndexes: 方法。
- -get<Key>:range:。可选,但是能获得额外的收益。相当于 NSArray 的 getObjects:range:。
Mutable Index Accessor
对于可变的版本,只需额外实现几个方法即可。
- -insertObject:in<Key>AtIndex: or -insert<Key>:atIndexes:至少实现一个。
- -removeObjectFrom<Key>AtIndex: or -remove<Key>AtIndexes:也是至少实现一个。
- -replaceObjectIn<Key>AtIndex:withObject: or -replace<Key>AtIndexes:with<Key>: 可选的,实现了能提高性能。
可以看出,这些方法在 NSMutableArray 中都有对应的实现。
无序的访问器模式
无序的存取器方法给可变的对象提供了一套访问机制。对象很可能是 NSSet 或 NSMutableSet 的实例。
Getter 需要实现的方法
- -countOf<Key>
- -enumeratorOf<Key>
- -memberOf<Key>:
Mutable 需要实现的方法
- -add<Key>Object: or -add<Key>:
- -remove<Key>Object: or -remove<Key>:
- -intersect<Key>:
Key Value 验证
KVC 为验证属性值提供了一致的 API。验证机制提供了一个类,使得有机会接受一个值,提供一个替换值,或者否认一个新值并给出错误原因。
通过验证方法,当 setValueForKey 方法传入一个新值时,我们有机会对这个值进行检查,然后来做一些处理。这样子对于值的验证集中在验证方法中,外界的业务逻辑处理变得很清楚。
举个例子:
验证方法命名习惯
验证方法的命名格式为 validate<Key>:error:
// 假如当前对象有属性 name
- (BOOL)validateName:(id *)iovalue error:(NSError **)error {
// 方法实现
}
实现一个验证方法
上面的验证方法提供了两个参数的引用:需要验证的值以及需要返回的错误信息。
对于上述方法可能有三个结果:
- 验证成功,返回 YES 并且不改变 error 对象。
- 值无法通过验证并且不能根据其创建合法的值,这时需要返回 NO 并且附上具体的 NSError
- 能够根据传入的值创建正确的值,将其返回即可。
具体可以看下官网文档中的示例:
-(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
// The name must not be nil, and must be at least two characters long.
if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
if (outError != NULL) {
NSString *errorString = NSLocalizedString(@"A Person's name must be at least two characters long",@"validation: Person, too short name error");
NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorString};
*outError = [[NSError alloc] initWithDomain:PERSON_ERROR_DOMAIN
code:PERSON_INVALID_NAME_CODE
userInfo:userInfoDict];
}
return NO;
}
return YES;
}
如果值无法通过验证时,需要首先检查 outError 参数是否为 nil,如果不是,需要将其设置为正确的值。
调用验证方法
可以直接调用该方法或者通过 validateValue:forKey:error: 指定 key 。将默认的去查找并匹配该 key 。如果找到了对应的方法,将按照其返回作为结果。如果未找到,将返回 YES 作为结果。
自动验证
一般来说,并不会自动调用验证方法,只有在使用 CoreData ,数据保存时会自动调用。
验证方法给我们提供了一种纠正错误的机会,例如这里传入待检查的参数是人名字符串,我们可以在这里将空格过滤掉,然后返回没有空格的名字。并且判断是否含有非法字符串,如果有非法字符串,就直接返回 NO 表示验证不能通过。
验证常量
验证方法默认参数是对象,对于常量和结构体需要单独做处理。