您的位置:首页 > 产品设计 > UI/UE

Key-Value Coding

2016-02-12 16:49 393 查看

介绍

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依赖的自己的那套访问器实现。两者可以一起使用,不会干扰对方,如下所示:

// KVC
MyClass *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:
的重写来实现处理成有意义的值。而结构体和标量对应对象的关系表如下:







集合操作符

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



在常规的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 = [arrayOfTransactionsArrays
valueForKeyPath:@"@unionOfArrays.payee"];

// 对于集合NSSet操作类型不一一列举了。


访问器搜索实现细节

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

先搜索接收者是否存在
-set<Key>
访问器,如果存在则调用。

如果没有访问器存在,且接收者类方法
accessInstanceVariablesDirectly
返回YES。那么按顺序搜索符合模式
_<key>
,
_is<Key>
,
<key>
,
is<Key>
的实例变量

如果没有访问器和实例变量,则从Non-Object Values中提取值。

如果以上方法都没办法执行,则
setValue:forUndefinedKey:
被执行。

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

先搜索接收者是否存在
-get<Key>
,
-<key>
,
-is<Key>
如果能搜索到,则执行。返回值一定是对象,如果遇到标量,结构体则会进行自动装箱。

如果不存在1的情况则继续搜索
-countOf<Key>
,
objectIn<Key>AtIndex:
,
<key>AtIndexes:
,后两者至少实现一个。

如果不存在2的情况则继续搜索
-countOf<Key>
,
-enumeratorOf<Key>
,
memberOf<Key>:


如果不存在3的情况,且
accessInstanceVariablesDirectly
返回YES,则按照顺序搜索
_<key>, _is<Key>
,
<key>
,
is<Key>
模式的实例变量

如果以上情况都不符合,则
valueForUndefinedKey:
会被调用。

* 更多关于搜索内容(如有序集合,无序集合等内容)参考苹果官方开发文档Key-Value Coding *
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: