iOS开发IOS

KVC(Key Value Coding)- Part 1

2015-09-27  本文已影响102人  WellCheng

什么是 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 集合方法,类的外面在调用时,根本感觉不到类里面使用 KVC 实现的。

这些思想得用具体的代码实现一下才能体会到 KVC 的特性。

这里有两种差异较大的集合存取器。

有序的集合

有序的集合关系中,存在计总、取回、添加和替换等操作。通常这种关系是 NSArray 或 NSMutableArray 的实例。

Getter

为了支持只读的访问属性:

Mutable Index Accessor

对于可变的版本,只需额外实现几个方法即可。

可以看出,这些方法在 NSMutableArray 中都有对应的实现。

无序的访问器模式

无序的存取器方法给可变的对象提供了一套访问机制。对象很可能是 NSSet 或 NSMutableSet 的实例。

Getter 需要实现的方法

Mutable 需要实现的方法

Key Value 验证

KVC 为验证属性值提供了一致的 API。验证机制提供了一个类,使得有机会接受一个值,提供一个替换值,或者否认一个新值并给出错误原因。

通过验证方法,当 setValueForKey 方法传入一个新值时,我们有机会对这个值进行检查,然后来做一些处理。这样子对于值的验证集中在验证方法中,外界的业务逻辑处理变得很清楚。

举个例子:

验证方法命名习惯

验证方法的命名格式为 validate<Key>:error:

// 假如当前对象有属性 name
- (BOOL)validateName:(id *)iovalue error:(NSError **)error {
    // 方法实现
}

实现一个验证方法

上面的验证方法提供了两个参数的引用:需要验证的值以及需要返回的错误信息。

对于上述方法可能有三个结果:

  1. 验证成功,返回 YES 并且不改变 error 对象。
  2. 值无法通过验证并且不能根据其创建合法的值,这时需要返回 NO 并且附上具体的 NSError
  3. 能够根据传入的值创建正确的值,将其返回即可。
    具体可以看下官网文档中的示例:
-(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 表示验证不能通过。

验证常量

验证方法默认参数是对象,对于常量和结构体需要单独做处理。

上一篇 下一篇

猜你喜欢

热点阅读