KVC(Key Value Coding)- Part 1

什么是 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 {
    // 方法实现
}

实现一个验证方法

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

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

  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 表示验证不能通过。

验证常量

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容