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简单属性的方法
[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访问器关联key和对应的属性,这一节就是介绍这个重要的KVC访问器方法。其模式如下:
-set
-
这里
对应的valueForKey:
的关键字,如果对象属性name,不想定义成用@“name”检索属性值,而是用@“myName”去检索那么对应的KVC访问器方法应该设置成如下:
-(void)setMyName: (NSString *) name
{
self.name = name;
}
-(NSString *)myName
{
return self.name;
}
当然,如果属性是个布尔值,它的KVC getter通常我们按照习惯会是-is
的形式。这样也是只支持的
- (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
和-
去实现,那样只能对集合对象操作,而无法对其存储的元素操作。如果需要对其存储的元素操作,需要用到集合访问器代理mutableArrayValueForKey:
或者mutableSetValueForKey:
。
实现集合访问器或者叫做mutable访问器(mutable accessor),可以有很多好处:
集合访问器有两种类型:索引访问器(index accessor)和无序访问器(unordered accessor)。前者针对NSArray这样的有序集合,而后者针对NSSet这样的无序集合。
你可以通过索引访问器来获得有序集合的内容数据,通过mutable访问器来提供一个修改集合属性的接口。
getter索引访问器需要实现如下内容:
-countOf
-objectInAtIndex:
或者 -AtIndexes:
get:range:
假设某个对象有个属性是employees,其类型为NSArray。它的索引访问器需要如下实现代码:
//-countOf
- (NSUInteger)countOfEmployees {
return [self.employees count];
}
//-objectInAtIndex:
- (id)objectInEmployeesAtIndex:(NSUInteger)index {
return [employees objectAtIndex:index];
}
//-AtIndexes:
- (NSArray *)employeesAtIndexes:(NSIndexSet *)indexes {
return [self.employees objectsAtIndexes:indexes];
}
//get: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:inAtIndex:
或者-insert:atIndexes:
-removeObjectFromAtIndex:
或者-removeAtIndexes:
-replaceObjectInAtIndex:withObject:
或者-replaceAtIndexes:with:
同样假设对象拥有一个NSArray类型的employees属性,其实现mutable索引访问器代码如下:
// -insertObject:inAtIndex:
- (void)insertObject:(Employee *)employee inEmployeesAtIndex:(NSUInteger)index {
[self.employees insertObject:employee atIndex:index];
return;
}
// -insert:atIndexes:
- (void)insertEmployees:(NSArray *)employeeArray atIndexes:(NSIndexSet *)indexes {
[self.employees insertObjects:employeeArray atIndexes:indexes];
return;
}
// -removeObjectFromAtIndex:
- (void)removeObjectFromEmployeesAtIndex:(NSUInteger)index {
[self.employees removeObjectAtIndex:index];
}
// -removeAtIndexes:
- (void)removeEmployeesAtIndexes:(NSIndexSet *)indexes {
[self.employees removeObjectsAtIndexes:indexes];
}
// -replaceObjectInAtIndex:withObject:
- (void)replaceObjectInEmployeesAtIndex:(NSUInteger)index
withObject:(id)anObject {
[self.employees replaceObjectAtIndex:index withObject:anObject];
}
// -replaceAtIndexes:with:
- (void)replaceEmployeesAtIndexes:(NSIndexSet *)indexes
withEmployees:(NSArray *)employeeArray {
[self.employees replaceObjectsAtIndexes:indexes withObjects:employeeArray];
}
上面讲的都是对有序集合的操作,接下来分析对无序集合的访问
getter无序访问器其实现如下:
-countOf
-enumeratorOf
-memberOf:
假设对象拥有NSSet的transactions属性:
// -countOf
- (NSUInteger)countOfTransactions {
return [self.transactions count];
}
// -enumeratorOf
- (NSEnumerator *)enumeratorOfTransactions {
return [self.transactions objectEnumerator];
}
// -memberOf:
- (Transaction *)memberOfTransactions:(Transaction *)anObject {
return [self.transactions member:anObject];
}
mutable无序访问器实现如下方法:
-addObject:
或者-add:
-removeObject:
或者-remove:
-intersect:
具体实现如下:
// -addObject:
- (void)addTransactionsObject:(Transaction *)anObject {
[self.transactions addObject:anObject];
}
// -add:
- (void)addTransactions:(NSSet *)manyObjects {
[self.transactions unionSet:manyObjects];
}
// -removeObject:
- (void)removeTransactionsObject:(Transaction *)anObject {
[self.transactions removeObject:anObject];
}
// -remove:
- (void)removeTransactions:(NSSet *)manyObjects {
[self.transactions minusSet:manyObjects];
}
// -intersect:
- (void)intersectTransactions:(NSSet *)otherObjects {
return [self.transactions intersectSet:otherObjects];
}
键值验证就是在设置值之前的一种“防御”措施。提供在设置之前自定义验证其请求合理性,其形式为validate
, 如下代码是对属性name的验证代码:
-(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {
// Implementation specific code.
return ...;
}
其中validate
有两个参数,第一个参数则为用户请求设置的值。第二个参数则是错误指针,包含错误信息。其具体实现可参考如下步骤:
验证实例代码如下:
-(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
方法去执行验证。如果不能找到对应的方法,则会返回YES,并且验证值。
PS: 在
-set
绝对不要调用验证方法,否则会进入死循环。
如何自动验证?其实就KVC是不支持自动验证的,因为那是业务逻辑的责任,如果非要支持自动验证,应该借助于Cocoa binding这个技术。另外还有Core Data下当managedObject上下文被保存的时候,也会自动验证。
对于attribute和to-one relatioship按照如下方式检查是否遵守KVC:
-
, -is
或者拥有实例变量
或 _
。虽然习惯上遵守小写字母开头,但是KVC也是支持大小字母开头的例如URL。-set
-validate:error:
-set
一定不能调用验证代码对于有序可索引的to-many relationship(e.g. NSArray)的实现如下:
-
或者拥有实例标量
或_
-countOf
以及至少实现 -objectInAtIndex:
和-AtIndexes:
中的一个-get:range:
在适当的地方来提高性能如果有序可索引的to-many relationship还需支持更改内容,还需要实现如下方法:
-insertObject:inAtIndex:
或 -insert:atIndexes:.
-removeObjectFromAtIndex:
或 -removeAtIndexes:
-replaceObjectInAtIndex:withObject:
或-replaceAtIndexes:with:
对于无序不可索引的to-many relationship(e.g. NSSet)的实现如下:
-
或者拥有实例标量
或_
-countOf
, -enumeratorOf
,-memberOf:
对于无序不可索引的to-many relationship还需支持更改内容,还需实现如下方法:
-addObject:
,-add:
中的一个-removeObject:
,-remove
中的一个-intersect:
和-set:
由于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
访问器,如果存在则调用。accessInstanceVariablesDirectly
返回YES。那么按顺序搜索符合模式_
, _is
,
, is
的实例变量setValue:forUndefinedKey:
被执行。valueForKey:
的访问器搜索顺序默认实现方式如下:
-get
, -
, -is
如果能搜索到,则执行。返回值一定是对象,如果遇到标量,结构体则会进行自动装箱。-countOf
,objectInAtIndex:
, AtIndexes:
,后两者至少实现一个。-countOf
, -enumeratorOf
, memberOf:
accessInstanceVariablesDirectly
返回YES,则按照顺序搜索_, _is
,
, is
模式的实例变量valueForUndefinedKey:
会被调用。* 更多关于搜索内容(如有序集合,无序集合等内容)参考苹果官方开发文档Key-Value Coding *