Key-Value Coding

来源:互联网 发布:淘宝纸盒 编辑:程序博客网 时间:2024/04/29 04:26

介绍

Key-Value Coding简称KVC,中文名为键值编码。它是一种利用字符串间接访问对象属性的方法。而这个字符串便就是键。访问器,即setter和getter,也是一种间接访问对象属性的方法,只不过在有些场合更加适合使用KVC。虽然KVC在业务逻辑中很少会使用,但在Key-Value Observing,Core Data, Cocoa bindings, scripatability这些模式或者高级特性的基础,因此KVC很重要。

在如下场合,利用KVC明显比利用访问器来得方便。假设一个要从model层获取所有联系人显示到NSTableView上(Cocoa里的列表)。最普通的方式如下:

- (id)tableView:(NSTableView *)tableview      objectValueForTableColumn:(id)column row:(NSInteger)row {    Person *person = [contact objectAtIndex:row];    if ([[column identifier] isEqualToString:@"name"]) {        return [person name];    }    if ([[column identifier] isEqualToString:@"age"]) {        return [person age];    }    if ([[column identifier] isEqualToString:@"favoriteColor"]) {        return [person favoriteColor];    }    // And so on.}

仔细想想,如果一个person的属性很多,那么代码会变得十分庞大,并且很难维护,如果利用KVC则会简洁得多

- (id)tableView:(NSTableView *)tableview      objectValueForTableColumn:(id)column row:(NSInteger)row {    Person *person = [contact objectAtIndex:row];    return [person valueForKey:[column identifier]];}

PS: 这里不理解KVC没关系,可以看完后面的实现,再来体会这里。

相关术语

在介绍如何实现KVC之前,先介绍几个接下来要使用的术语:

  • attribute. 它是一个简单属性,不存在一对一关系,一对多关系,只是描述某个事物的一个方面,不和其他对象产生任何联系。例如:标量,字符串,布尔值,NSNumber以及其他不可变的对象如NSColor等。
  • to-one relationship. 它反映的一对一关系属性,与之对应的对象和它独立变化。例如:UIView的superView就是一个to-one relationship属性。
  • to-many relationship. 它反映的是一对多属性关系,通常用集合来表示这样的关系。例如:NSArray, NSSet等。当然也可以利用后面介绍的方法,来自定义KVC的访问方式,在宏观上反映出一对多属性关系。
  • key. key就是一个唯一标识对象属性的字符串。通常就是属性名,如person的name属性,则key=”name”。当然可以自定义,它与具体KVC的访问方法的实现或者实例变量有关,后面会介绍。
  • key path. key path是利用“.”分割的一串字符串,用来标识一系列对象属性的层层穿越。例如一个国家有好多城市,一个城市有很多街道,则如果我拥有国家这个对象country,要访问到这个国家的中的某条街道,则key path=”country.city.street”

具体实现

获取attribute简单属性的方法

[object valueForKey: @"keywords"];

object: 拥有属性的对象。
keywords: 唯一标识属性的字符串key。

举例,例如要访问一个人person的名字name属性.

[person valueForKey: @"name"];

当然除了这样,还需要通过指定对应的访问器,来将key和属性产生关系。

-(void)setName: (NSString *)name{    self.name = name;}-(NSString *)name{    return self.name;}

看起来和setter,getter几乎是一模一样的,没错,就是一模一样。KVC的访问器,和关联属性的key有关。其内容在后面一节介绍。这里你仅需知道,它不是一般意义上的setter,getter访问器。

如果没有指定KVC访问器,那么接收对象会给自己发一个valueForUndefinedKey:消息。而这个消息在NSKeyValueCoding中定义,其默认实现是抛出一个NSUndefinedKeyException异常。你可以通过重写这个方法,来达到自定义需求。

相类似的,利用key path实现:

[object valueForKeyPath: @"parentKey.subKey"];

object: 拥有属性的对象。
parentKey.subKey: 唯一标识属性路径的字符串key path。

举个例子,访问一个国家的某个城市的,某个街道

[country valueForKeyPath: @"city.street"];

在key path的每个子路径都需要实现与之对应的KVC访问器,如果其中任何一个没有与之对应的访问器,则出出发valueForUndefinedKey:消息。

PS: 如果country的城市属性是一个集合citys,而每个城市又有街道的集合streets,则通过”citys.streets”这个key path会返回这个国家中所有城市的所有街道。

当然如果你想在一个语句中获取多个属性值,则利用dictionaryWithValuesForKeys:方法会返回一个NSDictionary,里面包含属性的key和对应属性的值。

PS: 在集合对象中,NSArray, NSSet, NSDictionary不能包含nil来表示空值,因为nil表示集合结束。如果要表示空值,请用NSNull,NSNull是一个单例类。[NSNull null]来访问单例对象。还有dictionaryWithValuesForKeys:setValuesForKeysWithDictionary: 提供了对NSNull和nil的自动拆箱和装箱的桥接。

设置attribute简单属性的方法

[object setValue: valueObject forKey: @"key"];

举个例子

[person setValue: @"xiaoming" forKey: @"name"];

同样的道理,需要关联KVC访问器,如果没有关联访问器,接收消息对象会给自己发送setValue:forUndefinedKey:消息,其默认实现是抛出NSUndefinedKeyException异常。

对key path的设置以及同时设置多个属性的方式也是和访问差不多的

[object setValue: valueObject forKeyPath: @"parentKey.subKey"];[object setValuesForKeysWithDictionary: @{"key1": value1, ....}];

点语法和KVC

点语法和KVC两者很类似,只不过原理不同。点语法依赖的是标准访问器setter,getter的实现,KVC依赖的自己的那套访问器实现。两者可以一起使用,不会干扰对方,如下所示:

// KVCMyClass *myInstance = [[MyClass alloc] init];NSString *string = [myInstance valueForKey:@"stringProperty"];[myInstance setValue:@2 forKey:@"integerProperty"];// 点语法MyClass *anotherInstance = [[MyClass alloc] init];myInstance.linkedInstance = anotherInstance;myInstance.linkedInstance.integerProperty = 2;

两者效果是一样的。

KVC访问器方法

前面多次提到,需要用KVC访问器关联key和对应的属性,这一节就是介绍这个重要的KVC访问器方法。其模式如下:

-set<Key>-<Key>

这里<Key>对应的valueForKey:的关键字,如果对象属性name,不想定义成用@“name”检索属性值,而是用@“myName”去检索那么对应的KVC访问器方法应该设置成如下:

-(void)setMyName: (NSString *) name{    self.name = name;}-(NSString *)myName{    return self.name;}

当然,如果属性是个布尔值,它的KVC getter通常我们按照习惯会是-is<Key>的形式。这样也是只支持的

- (BOOL)isHidden {   // Implementation specific code.   return ...;}

当遇到不支持nil的数据类型时,比如说BOOL,那么还得实现接收对象的setNilValueForKey:方法:

- (void)setNilValueForKey:(NSString *)theKey {    if ([theKey isEqualToString:@"hidden"]) {        [self setValue:@YES forKey:@"hidden"];    }    else {        [super setNilValueForKey:theKey];    }}

这里相当于当给hidden,调用setNilValueForKey: @"hidden"的时候,hidden会被设置为YES。

当对于关系到集合类型属性的时候,也就是to-many relationship的属性。如果只用-set<Key>-<Key>去实现,那样只能对集合对象操作,而无法对其存储的元素操作。如果需要对其存储的元素操作,需要用到集合访问器代理mutableArrayValueForKey:或者mutableSetValueForKey:
实现集合访问器或者叫做mutable访问器(mutable accessor),可以有很多好处:

  • 对一对多关系的高性能操作
  • 除了用NSArray和NSSet还可以实现合适的集合访问器,宏观上提供自定义的集合类型
  • 在Key-Value Observing中可以利用集合访问器直接通知观察者。

集合访问器有两种类型:索引访问器(index accessor)和无序访问器(unordered accessor)。前者针对NSArray这样的有序集合,而后者针对NSSet这样的无序集合。

你可以通过索引访问器来获得有序集合的内容数据,通过mutable访问器来提供一个修改集合属性的接口。

getter索引访问器需要实现如下内容:

  • -countOf<Key>
  • -objectIn<Key>AtIndex: 或者 -<Key>AtIndexes:
  • get<Key>:range:

假设某个对象有个属性是employees,其类型为NSArray。它的索引访问器需要如下实现代码:

//-countOf<Key>- (NSUInteger)countOfEmployees {    return [self.employees count];}//-objectIn<Key>AtIndex:- (id)objectInEmployeesAtIndex:(NSUInteger)index {    return [employees objectAtIndex:index];}//-<Key>AtIndexes:- (NSArray *)employeesAtIndexes:(NSIndexSet *)indexes {    return [self.employees objectsAtIndexes:indexes];}//get<Key>:range:- (void)getEmployees:(Employee * __unsafe_unretained *)buffer range:(NSRange)inRange {    // Return the objects in the specified range in the provided buffer.    // For example, if the employees were stored in an underlying NSArray    [self.employees getObjects:buffer range:inRange];}

mutable索引访问器需要实现如下内容
mutable索引访问器相当于对普通属性的setter访问器。提供mutable索引访问器可以给你带来更加高效的集合属性内容的操作。

  • -insertObject:in<Key>AtIndex:或者-insert<Key>:atIndexes:
  • -removeObjectFrom<Key>AtIndex:或者-remove<Key>AtIndexes:
  • -replaceObjectIn<Key>AtIndex:withObject:或者-replace<Key>AtIndexes:with<Key>:

同样假设对象拥有一个NSArray类型的employees属性,其实现mutable索引访问器代码如下:

// -insertObject:in<Key>AtIndex:- (void)insertObject:(Employee *)employee inEmployeesAtIndex:(NSUInteger)index {    [self.employees insertObject:employee atIndex:index];    return;}// -insert<Key>:atIndexes:- (void)insertEmployees:(NSArray *)employeeArray atIndexes:(NSIndexSet *)indexes {    [self.employees insertObjects:employeeArray atIndexes:indexes];    return;}// -removeObjectFrom<Key>AtIndex:- (void)removeObjectFromEmployeesAtIndex:(NSUInteger)index {    [self.employees removeObjectAtIndex:index];}// -remove<Key>AtIndexes:- (void)removeEmployeesAtIndexes:(NSIndexSet *)indexes {    [self.employees removeObjectsAtIndexes:indexes];}// -replaceObjectIn<Key>AtIndex:withObject:- (void)replaceObjectInEmployeesAtIndex:(NSUInteger)index                             withObject:(id)anObject {    [self.employees replaceObjectAtIndex:index withObject:anObject];}// -replace<Key>AtIndexes:with<Key>:- (void)replaceEmployeesAtIndexes:(NSIndexSet *)indexes                    withEmployees:(NSArray *)employeeArray {    [self.employees replaceObjectsAtIndexes:indexes withObjects:employeeArray];}

上面讲的都是对有序集合的操作,接下来分析对无序集合的访问

getter无序访问器其实现如下:

  • -countOf<Key>
  • -enumeratorOf<Key>
  • -memberOf<Key>:

假设对象拥有NSSet的transactions属性:

// -countOf<Key>- (NSUInteger)countOfTransactions {    return [self.transactions count];}// -enumeratorOf<Key>- (NSEnumerator *)enumeratorOfTransactions {    return [self.transactions objectEnumerator];}// -memberOf<Key>:- (Transaction *)memberOfTransactions:(Transaction *)anObject {    return [self.transactions member:anObject];}

mutable无序访问器实现如下方法:

  • -add<Key>Object:或者-add<Key>:
  • -remove<Key>Object:或者-remove<Key>:
  • -intersect<Key>:

具体实现如下:

// -add<Key>Object:- (void)addTransactionsObject:(Transaction *)anObject {    [self.transactions addObject:anObject];}// -add<Key>:- (void)addTransactions:(NSSet *)manyObjects {    [self.transactions unionSet:manyObjects];}// -remove<Key>Object:- (void)removeTransactionsObject:(Transaction *)anObject {    [self.transactions removeObject:anObject];}// -remove<Key>:- (void)removeTransactions:(NSSet *)manyObjects {    [self.transactions minusSet:manyObjects];}// -intersect<Key>:- (void)intersectTransactions:(NSSet *)otherObjects {    return [self.transactions intersectSet:otherObjects];}

键值验证

键值验证就是在设置值之前的一种“防御”措施。提供在设置之前自定义验证其请求合理性,其形式为validate<Key>:error:, 如下代码是对属性name的验证代码:

-(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {    // Implementation specific code.    return ...;}

其中validate<Key>:error:有两个参数,第一个参数则为用户请求设置的值。第二个参数则是错误指针,包含错误信息。其具体实现可参考如下步骤:

  • 如果请求设置值是合理的,则返回YES不报错。
  • 如果请求设置值不合理,则返回NO,并将相关错误信息通过错误指针带出。
  • 如果一个新的合理对象被创建,并且赋值给对应属性,除了返回YES之外,还要返回对应的对象。错误不是必须返回的。

验证实例代码如下:

-(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;}

PS: 如果验证方法返回NO,那么必须检查outError是不是NULL,如果NULL则必须给予合理的NSError对象赋值。

如何调用验证方法?第一种是直接调用验证方法,第二种则是通过validateValue:forKey:error:,它会自动寻找到对应的validate<Key>:error:方法去执行验证。如果不能找到对应的方法,则会返回YES,并且验证值。

PS: 在-set<Key>绝对不要调用验证方法,否则会进入死循环。

如何自动验证?其实就KVC是不支持自动验证的,因为那是业务逻辑的责任,如果非要支持自动验证,应该借助于Cocoa binding这个技术。另外还有Core Data下当managedObject上下文被保存的时候,也会自动验证。

KVC Compliance

对于attribute和to-one relatioship按照如下方式检查是否遵守KVC:

  • 实现方法名字中带有-<Key>, -is<Key> 或者拥有实例变量 <key>_<key>。虽然习惯上遵守小写字母开头,但是KVC也是支持大小字母开头的例如URL。
  • 如果属性可更改,那么必须实现-set<Key>
  • 如果类需要支持设置值前的验证,那么必须实现-validate<Key>:error:
  • -set<Key>一定不能调用验证代码

对于有序可索引的to-many relationship(e.g. NSArray)的实现如下:

  • 实现-<Key>或者拥有实例标量<key>_<key>
  • 或者实现-countOf<Key> 以及至少实现 -objectIn<Key>AtIndex:-<key>AtIndexes:中的一个
  • 当然利用-get<Key>:range:在适当的地方来提高性能

如果有序可索引的to-many relationship还需支持更改内容,还需要实现如下方法:

  • -insertObject:in<Key>AtIndex:-insert<Key>:atIndexes:.
  • -removeObjectFrom<Key>AtIndex:-remove<Key>AtIndexes:
  • 如果要替换对象,你还可以选择性的实现-replaceObjectIn<Key>AtIndex:withObject:或-replace<Key>AtIndexes:with<Key>:

对于无序不可索引的to-many relationship(e.g. NSSet)的实现如下:

  • 实现-<key>或者拥有实例标量<key>_<key>
  • 或者实现-countOf<Key>, -enumeratorOf<Key>,-memberOf<Key>:

对于无序不可索引的to-many relationship还需支持更改内容,还需实现如下方法:

  • 至少实现-add<Key>Object:-add<Key>:中的一个
  • 至少实现-remove<Key>Object:,-remove<Key>中的一个
  • 可选实现-intersect<Key>:-set<Key>:

对于标量和结构体的支持

由于KVC是基于对象的一种机制,valueForKey:setValue:forKey:遇到标量和结构体时会自动封装成对象。而对于nil值利用前面提过的setNilValueForKey:的重写来实现处理成有意义的值。而结构体和标量对应对象的关系表如下:

NSNumber对应标量1

NSNumber对应标量2

NSNumber对应标量3

集合操作符

对于一些集合类型的属性,可以对比数据库中的聚合函数,对集合里的内容进行操作,其表达式定义如下:

集合操作符表达式

在常规的key path的要操作集合前加@操作内容,例如统计数量@count,求和@sum

举一些例子:

// 求平均值NSNumber *transactionAverage = [transactions valueForKeyPath:@"@avg.amount"];// 统计数量NSNumber *numberOfTransactions = [transactions valueForKeyPath:@"@count"];// 求最大值NSDate *latestDate = [transactions valueForKeyPath:@"@max.date"];// 求最小值NSDate *earliestDate = [transactions valueForKeyPath:@"@min.date"];// 求和NSNumber *amountSum = [transactions valueForKeyPath:@"@sum.amount"];

除了聚合函数之外,还有支持关系运算的对象操作符号,其使用方法和前者一样,只不过含义不同,举一些例子:

// 去重合并所有支付账单NSArray *payees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];// 非去重合并支付账单NSArray *payees = [transactions valueForKeyPath:@"@unionOfObjects.payee"];

还有数组以及集合操作符

// 返回所有交易事务中去重支付账单NSArray *payees = [arrayOfTransactionsArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];// 返回所有交易事务中非去重支付账单NSArray *payees = [arrayOfTransactionsArraysvalueForKeyPath:@"@unionOfArrays.payee"];// 对于集合NSSet操作类型不一一列举了。

访问器搜索实现细节

setValue:forKey:的访问器搜索顺序默认实现方式如下:

  1. 先搜索接收者是否存在-set<Key>访问器,如果存在则调用。
  2. 如果没有访问器存在,且接收者类方法accessInstanceVariablesDirectly返回YES。那么按顺序搜索符合模式_<key>, _is<Key>, <key>, is<Key>的实例变量
  3. 如果没有访问器和实例变量,则从Non-Object Values中提取值。
  4. 如果以上方法都没办法执行,则setValue:forUndefinedKey:被执行。

valueForKey:的访问器搜索顺序默认实现方式如下:

  1. 先搜索接收者是否存在-get<Key>, -<key>, -is<Key>如果能搜索到,则执行。返回值一定是对象,如果遇到标量,结构体则会进行自动装箱。
  2. 如果不存在1的情况则继续搜索-countOf<Key>,objectIn<Key>AtIndex:, <key>AtIndexes:,后两者至少实现一个。
  3. 如果不存在2的情况则继续搜索-countOf<Key>, -enumeratorOf<Key>, memberOf<Key>:
  4. 如果不存在3的情况,且accessInstanceVariablesDirectly返回YES,则按照顺序搜索_<key>, _is<Key>, <key>, is<Key>模式的实例变量
  5. 如果以上情况都不符合,则valueForUndefinedKey:会被调用。

* 更多关于搜索内容(如有序集合,无序集合等内容)参考苹果官方开发文档Key-Value Coding *

0 0
原创粉丝点击