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:
的重写来实现处理成有意义的值。而结构体和标量对应对象的关系表如下:
集合操作符
对于一些集合类型的属性,可以对比数据库中的聚合函数,对集合里的内容进行操作,其表达式定义如下:
在常规的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:
的访问器搜索顺序默认实现方式如下:
- 先搜索接收者是否存在
-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 *
- Key-Value Coding 详解.
- Key-Value coding (KVC)
- kvc (key value coding)
- KVC (Key-Value Coding)
- Key-Value Coding
- Key-Value Coding (KVC)
- Key-Value-Coding(KVC)
- Key-Value Coding
- Key-Value Coding; Key-Value Observing
- Key-Value Coding 01 - What is key-Value Coding
- cocoa's Key-value coding
- KVC(Key-value coding)机制
- Objective-C Key-Value-Coding
- IOS 学习 Key-value coding
- KVC (Key -Value Coding)
- What Is Key-Value Coding?
- iOS KVC(Key-Value Coding)
- Key-Value Coding 中文翻译-2
- JS固定侧边栏教程总结
- Codeforces AIM-TECH round div2 624AB 623ABC
- Github学习记录
- 用十条命令在一分钟内检查Linux服务器性能
- 博客开张
- Key-Value Coding
- Linux环境下使用V4L2+opencv以MJPEG格式读取USB摄像头并实时显示
- 1002. A+B for Polynomials (25)
- 蓝桥杯 - 最大乘积
- hdu 5296 Annoying problem
- android:从另外一个activity中返回数据
- pig实战演练:手机流量统计
- Ubuntu14.04 上使用 Nginx 部署 Flask 应用
- The Java™ Tutorials — Concurrency :Synchronized Methods 同步方法