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 *
相关文章推荐
- iOS 【UIKit-转换坐标系(1)】
- durpal8+Acquia Dev Desktop安装问题处理
- hdu1503Advanced Fruits【最长公共子序列】
- LeetCode 96 Unique Binary Search Trees不同的二叉搜索树的个数
- Building Maintainable Software-java篇之Write Simple Units of Code
- Educational Codeforces Round 7 A. Infinite Sequence 水题
- POJ 2623 Sequence Median(水~)
- 303. Range Sum Query - Immutable LeetCode
- UIView不接受触摸事件的三种情况:
- Educational Codeforces Round 7(D)构造(STL deque)
- Light OJ 1126 Building Twin Towers (DP)
- Building Maintainable Software-java篇之Write Short Units of Code
- deque 双端队列。
- PHP:数组操作函数array_count_values()的实现
- ELK setup guide
- Java中序列化的serialVersionUID作用
- 【CodeForces 618B】Guess the Permutation
- HDU 3732 Ahui Writes Word(01背包转化为多重背包)
- E - Number Sequence(第二季水)
- css3 Media Query IE6-8兼容问题