apple开发者文档关于KVC和KVO文档的一些渣翻——这里是原地址
Key-Value Coding Programming Guide
KVC编程介绍
介绍
本文档用来描述NSKeyValueCoding非正式接口,NSKeyValueCoding定义了一种允许app通过键值来间接访问一个对象的属性的机制,而不是直接调用被访问的对象的getter方法或者是实例对象的值
你可以通过阅读本文档理解如何在你的app中使用KVC,并且使之与其他的技术相兼容。KVC是在KVO模式、Cocoa bingdings、Core Data中的一项基本的技术,能够使你的app可支持applescript语言(AppleScript-able)。在阅读本文档前,你应该熟悉基本的Cocoa开发和Objective-C语言
文档目录:
- 什么是KVC?KVC的概述
- 常用于描述一个对象的属性的术语
- 使用KVC的基本规定
- 在你的类中应该实现的KVC的存取方法
- 键值验证
- 使用KVC必须实现的方法
- KVC支持的数据类型
- Collection操作和用法
- 如何确定适当的访问方法和实例变量
- 使用元数据定义对象和它们的属性间的关系
- 使用KVC的性能考虑
1、什么是KVC(Key-Value Coding)?KVC的概述
Key-Value Coding(键值编码)是间接存取一个对象的属性的一种机制,通过使用标识符字符串来定义属性,而不是直接使用对象的存取方法或者实例对象值。
存取方法,正如名字所展示,提供了你的app的数据模型的属性的值的存取方法。默认的存取方法有两个–get方法和set方法。get方法也叫getter,getter将返回一个属性的值。set方法也叫setter,setter可以设置一个属性的值。可以重载getter和setter方法来处理对象的属性及其它关系
在你的app中实现KVC兼容的存取方法非常重要。存取方法有助于数据封装和集成其他技术,例如KVC、Core Data、Cocoa bindings和脚本。KVC方法在大多数情况下可以简化你的代码
KVC的最基本的方法在OC语言的NSKeyValueCoding类的非正式接口中被定义,其默认的实现方法由NSObject提供。
KVC支持带有值的属性和标量类型和结构体。非对象参数和返回类型将会被检测和自动封装,解包,在下面这个链接有描述
我是链接
2、KVC和脚本
在Cocoa中,脚本支持被专门设计用于轻松地实现通过脚本访问它的模型对象——封装app的数据的对象。当用户执行一个applescript指令,结果将直接跳转到应用模型对象来完成工作
OS X的脚本很大程度上依赖KVC来完成自动完成Applescript指令的执行。在一个使用脚本的app里面,一个模型对象由一系列的它支持的键值定义组成。每个key代表对象的一个属性。脚本相关的key有文字、字体、文件和颜色。KVC API提供了一种通用和自动的方式来查询对象key对应的值,并为这些key设置新值。
当你在设计你的app时,你应该为你的对象模型定义一系列的keys并且在适配到他们相应的存取方法中(getter和setter)。然后当你为你的app定义脚本类时,你可以指定这写keys支持的脚本类。如果你的app支持KVC,那么你就可以在许多地方随意地使用脚本了。
在applescript中,在一个app中,对象层定义了模型对象的结构。大多数的appscript指令指定应用中的一个或者多个对象,通过深入到这个对象的父容器中获取子元素。你可以配置KVC在类描述中定义属性之间的关系。参见Describing property relationships这篇文章以查看更多细节。
Cocoa脚本支持利用KVC获取和设置脚本对象的信息。Objective-C非正式协议NSScriptKeyValueCoding 提供了KVC额外功能,包括通过多值key的index获取和设置键值并转换键值到一个合适的数据类型。
使用KVC简化代码
你可以在自己的代码中使用KVC方法。例如,在OS X NSTableView 和NSOutlineView对象都关联每个列的标示符字符串。通过标示符标示你希望显示的关键属性,你可以极大的简化你的代码
列表1中展示了NSTableView 代理方法不使用KVC的实现。列表2中展示了使用KVC返回相应值,使用列标示符作为key
列表1 不使用KVC实现数据源方法
列表1 不使用KVC实现数据源方法的列表
- (id)tableView:(NSTableView *)tableview
objectValueForTableColumn:(id)column row:(NSInteger)row {
ChildObject *child = [childrenArray objectAtIndex:row];
if ([[column identifier] isEqualToString:@"name"]) {
return [child name];
}
if ([[column identifier] isEqualToString:@"age"]) {
return [child age];
}
if ([[column identifier] isEqualToString:@"favoriteColor"]) {
return [child favoriteColor];
}
// And so on.
}
列表2 数据源使用KVC方法的列表
- (id)tableView:(NSTableView *)tableview
objectValueForTableColumn:(id)column row:(NSInteger)row {
ChildObject *child = [childrenArray objectAtIndex:row];
return [child valueForKey:[column identifier]];
}
术语(Terminology)
KVC定义了一些自己的术语
KVC可以被用于访问三种不同类型的对象值:属性、对一关系、对多关系。property指代这任意三种值。
属性(attribute):属性是一个简单的属性值,比如一个标量、字符串或者是一个bool值。值对象例如NSNumber和其他不可变类型比如NSColor同样也术语属性。
对一关系(to-one relationship):一个对象拥有多个属性,那么它就是一种多对一的关系。这些属性的改变并不会引起对象本身的改变,比如一个NSView的父视图就是一种对一关系。
对多关系(to-many relationship):通常是一个包含关系对象的Collection。NSArray 或NSSet 的实例通常用于这种集合。然而,通过实现中多重属性模式的访问器集合(Collection Accessor Patternsfor To-Many Properties)的KVC访问器,KVC允许你使用定制集合类并访问他们,仿佛他们是NSArray 或NSSet 。
KVC的基本原理
这个模块是讲述KVC的基本原理。
keys和key路径
一个key是一个对象的一个某一个属性的一个标识符字符串。通常来说,一个key对应了一个存取方法或者接受对象的实例变量。key必须使用ASCII编码,以小写字母开头,并且不包含空格。
一些keys的例子:payee,openingBalance,transactions或者amount
Key路径是是点分割key的字符串,用于指定对象属性遍历序列。第一个key的属性与接收者相关,每个后续关键key与前一个属性的值相关。
例如,key路径address.street可以从接收对象获取地址属性的值,并确定街道属性与地址对象相关。
使用KVC获取属性值
valueForKey:方法返回接收者相关指定key的值。如果没有指定key的访问器或实例变量,接收者发送自身valueForUndefinedKey:消息。 valueForUndefinedKey:默认实现有一个 NSUndefinedKeyException子类可以重写该行为。
valueForKeyPath:返回相关接收者指定key路径的值。Key路径序列中的任何对象中不兼容KVC属性key将接收到 valueForUndefinedKey:消息。
dictionaryWithValuesForKeys: 方法搜索接收者相关key数组值。返回的NSDictionary包含数组中的所有key的值
注意:集合对象,例如NSArray, NSSet和NSDictionary ,不能包含nil作为值。相反,你应该使用特定对象NSNull来代替nil。NSNull提供了实例代表对象属性值为nil。dictionaryWithValuesForKeys:和setValuesForKeysWithDictionary:的默认实现自动转换NSNull和nil,所以你的对象不需要显式的测试NSNull值。
当key路径返回的值包含很多属性的key,并且该key不是路径中最后的key,返回值是一个集合,包含所有key对应的值。例如,请求key路径transactions.payee 返回一个数组包含所有交易的所有payee对象。这对key路径中多数组通用使用。Key路径accounts.transactions.payee返回所有账户所有交易的所有payee对象。
使用KVC设置属性值
setValue:forKey:方法设置接收者相关特定key的值。setValue:forKey:的默认实现自动将NSValue对象拆包为标量和结构来分配他们的属性。参见标量结构帮助( Scalar and Structure Support)了解更多关于装包和拆包语法。
如果指定的key不存在,接收者发送setValue:forUndefinedKey:一个消息。setValue:forUndefinedKey:的默认实现有NSUndefinedKeyException,然而子类可以重写此方法来自定义的方式处理请求。
setValue:forKeyPath: 方法类似的方式实现,但它能够处理key路径。
最后,setValuesForKeysWithDictionary:设置接收者的属性值为指定字典,使用字典的key来识别属性。setValue:forKey: 默认实现,对每个键值对,按要求使用NSNull代替nil。
你应该考虑一个额外的问题,当试图设置一个非空属性为nil值时会发生什么。在这种情况下,接收者会发送setNilValueForKey: 消息。setNilValueForKey: 默认实现有NSInvalidArgumentException。你的应用可以覆盖整个方法来替代默认值或标示值,然后调用setValue:forKey:设置新值。
点语法和KVC
在OC中,点语法和KVC是相互独立的,无论你是否使用点语法,你都可以使用KVC,同样,无论你是否使用KVC你都可以使用点语法。在KVC中,点语法是用来划分key路径中的元素。当然,当你使用点语法访问一个属性时,你调用的的是接收者的标准访问器方法。
你可以使用KVC方法去访问一个属性,比如,对于给定的一个定义的类如下:
@interface MyClass
@property NSString *stringProperty;
@property NSInteger integerProperty;
@property MyClass *linkedInstance;
@end
你可以这样在一个实例中访问它的属性:
MyClass *myInstance = [[MyClass alloc] init];
NSString *string = [myInstance valueForKey:@"stringProperty"];
[myInstance setValue:@2 forKey:@"integerProperty"];
下面的代码展示点语法和KVC的key路径的不同
//这个是点语法
MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
myInstance.linkedInstance.integerProperty = 2;
//这个是Key path
MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
[myInstance setValue:@2 forKeyPath:@"linkedInstance.integerProperty"];
KVC访问器方法
为了使KVC能够正确地调用 valueForKey:, setValue:ForKey:, mutableArrayValueForKey:, mutableSetValueForKey:,你应该实现KVC的存取方法。
备注:在本节中,访问器模式会写成-set< key>:或者-< key>的格式。< key>表示你的属性的key的名字。你在实现自己的相应的方法时,应该把这些< key>或者< key>代替成确切的key。比如,在名字属性中,-set< key>:应该写成-setName:,-< key>应该写成-name。
常用的访问器模式
一个访问器方法的格式:返回一个属性是-< key>。-< key>方法将返回一个对象,标量或者一个数据结构体。备用的命名格式-is< key>支持Boolean类型的属性。
列表1展示了使用常用的命名hidden属性的方法,列表2展示了使用备用命名格式的方法。
//列表1 为一个hidden属性访问器命名常用格式
- (BOOL)hidden {
// Implementation specific code.
return ...;
}
//列表2 为一个hidden属性备用的访问器命名格式
- (BOOL)isHidden {
// Implementation specific code.
return ...;
}
为了使一个属性或者对一关系支持setValue:forKey,你必须实现一个set< key>格式的访问器。下面列表3是实现hidden属性的访问器方法:
//列表3 支持hidden属性的访问器命名格式
- (void)setHidden:(BOOL)flag {
// Implementation specific code.
return;
}
如果属性是一个非对象类型,你也必须实现一个合适的方法来表示一个空的值。当你想设置一个属性为空的时候,KVC中的setNilValueForKey:方法将被调用。这个方法将会在你的app中,在类中提供一些合适的默认值或者是没有相应的访问器的handle keys。
接下来的例子当hidden将被设置成为nil的时候,hidden会被设置为YES。它创建了一个NSNumber实例,包括了一个boolean值,然后使用setValue:forKey:来设置一个新的值。这将维持这个模型的封装,确保所有额外的应该改变这个值的行为都将发挥作用。这个比起直接调用访问器方法或者直接设置实例变量,被认为是更好的做法。
- (void)setNilValueForKey:(NSString *)theKey {
if ([theKey isEqualToString:@"hidden"]) {
[self setValue:@YES forKey:@"hidden"];
}
else {
[super setNilValueForKey:theKey];
}
}
对多属性的Collection的访问器模式
尽管你的app可以通过使用-< key>和-set< key>的访问器格式来实现对多关系属性,你通常应该只使用这些方法来创建集合对象。用于操作这个集合的呢哦荣的最佳方法应该是实现一个额外的访问器方法,我们称之为Collection访问器方法。你可以使用集合访问器方法,或者是一个由mutableArrayValueForkey:或者mutablesetvalueforkey方法返回的可变的Collection代理。
实现Collection访问器方法,比起最基本的getter,有下面许多优点:
- 当存在多对可变的对多关系时,性能将得到提高
- 对多关系可以通过实现相应的访问器Collection用NSArray或者NSSet建模,实现集合访问器方法可以在使用KVC方法时,区分数组或者集合
你可以直接使用Collection访问器方法来对KVO模式的Collection进行直接的修改。查看 KVO编程指南了解更多
Collection访问器方法有两个变种:为已经排序的对多关系准备的索引访问器和未排序并且不需要排序的关系的无序访问器。
索引访问器模式
在一个排序好的关系中,索引访问器方法定义了一种对象计数、恢复、增加和代替的机制。通常这个关系是一个NSArray或者是NSMutableArray的实例。任何对象可以像一个数组一样,实现这些方法和被操作。你不仅仅是简单地实现这些方法,你也可以直接在关系中调用这些对象。
从Collection或者可变访问器返回数据的索引访问器提供了一个接口方法mutableArrayValueForKey:用来修改集合。
获取索引访问器
为了对多关系的支持只读,你应该实现下面的方法:
- -countOf< key>.(必须实现)这个是一个类似NSArray的原始方法 count
- -objectIn< key>Atindex: 或者 < key>AtIndexes .其中一个必须实现,它们的作用就跟NSArray的方法 objectAtIndex 和 objectsAtIndexes:一样
- -get< key>:range:.这个方法是可选的,实现之后可以提高性能。这个方法跟NSArray的方法 getObject:range:一样。
-countOf< key>方法的实现将简单地返回一个NSUInteger类型的数据用来表示在对多关系里面的对象的数量。列表4的代码片段说明了-countOf< key>实现了对多关系中employees属性的具体实现。
//-count< key>实现例子
- (NSUInteger)countOfEmployees {
return [self.employees count];
}
方法-objectIn< key>AtIndex:返回在一个对多关系中的一个确切索引指向的对象。方法-< key>AtIndexes: 访问器将返回以格式通过NSIndexSet格式的索引确切地返回一个对象数组。上面两个方法只能有一个被实现。
列表5的代码片段实现了-objectIn< key>AtIndex:和-< key>AtIndexes:在对多关系的employees属性中的实现。
//列表5 -objectIn< key>AtIndex: 和-< key>AtIndexes:的实现
- (id)objectInEmployeesAtIndex:(NSUInteger)index {
return [employees objectAtIndex:index];
}
- (NSArray *)employeesAtIndexes:(NSIndexSet *)indexes {
return [self.employees objectsAtIndexes:indexes];
}
如果标准流程指明性能改善是必须的,那么你可以实现-get< key>:range:。你的访问器的实现应该在缓冲区给定的两个参数范围中返回对象。(其实就是一个指定了参数范围的返回方法)。
列表6展示了对多关系中的employees属性的-get< key>:range: 的实现
//-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];
}
可变索引访问器
索引访问器要支持一个可变的对多关系需要额外实现一些方法。实现可变索引访问器将允许你的app通过使用mutableArrayValueForKey:方法返回的数组代理更加便捷而有效地操作索引Collection。另外,通过实现这些方法,你的类的对多关系的属性将会遵守KVO.
注意:强烈建议你实现这些可变访问器而不是通过一个访问器直接返回一个可变的Collection。这些可变访问器在关系发生数据变化的时候将更加的有效率。
为了使一个有序的对多关系遵守KVO,你应该实现下列的方法:
- -insertObject:in< key>AtIndex:或者-insert< key>:atIndexes:.至少有其中一个方法必须实现,这个就类似于NSMutableArray方法里面的insertObject:atIndex和insertObjects:atIndexes:。
- removeObjectFrome< key>AtIndex:或者-remove< key>AtIndexes.至少有其中一个方法必须实现,这些方法分别类似于NSMutableArray里面的 removeObjectAtIndex: 和 removeObjectsAtIndexes:
- -replaceObjectIn< key>AtIndex:withObject: 或者 -replace< key>AtIndexes:with< key>:。可选的,当主流程对性能要求比较高时实现它。
-insertObject:in< key>AtIndex: 方法参数传入一个要插入的对象,一个表示索引位置的NSUInteger类型的index。-insert< key>:atIndexes:方法通过一组索引NSIndexSet类型的数组往Collection中插入一个对象数组。你只需要实现这两个方法的其中一个。
列表7展示了对多关系的employee属性的插入访问器的实现
//对多关系的employee属性的插入访问器的实现
- (void)insertObject:(Employee *)employee inEmployeesAtIndex:(NSUInteger)index {
[self.employees insertObject:employee atIndex:index];
return;
}
- (void)insertEmployees:(NSArray *)employeeArray atIndexes:(NSIndexSet *)indexes {
[self.employees insertObjects:employeeArray atIndexes:indexes];
return;
}
如果程序对性能要求比较高时,你也可以实现下面两个可选的代替访问器的其中一个,你实现-replaceObjectIn< key>AtIndex:withObject: 或者 -replace< key>AtIndexes:with< key>:,当一个对象在Collection中被代替时,这个方法会被调用,这个方法将直接替换对象,而不是先删除,在插入。
列表9展示了-replaceObjectIn< key>AtIndex:withObject: 和 -replace< key>AtIndexes:with< key>:在对多关系中employee属性的实现
- (void)replaceObjectInEmployeesAtIndex:(NSUInteger)index
withObject:(id)anObject {
[self.employees replaceObjectAtIndex:index withObject:anObject];
}
- (void)replaceEmployeesAtIndexes:(NSIndexSet *)indexes
withEmployees:(NSArray *)employeeArray {
[self.employees replaceObjectsAtIndexes:indexes withObjects:employeeArray];
}
无序访问器模式
无序访问器方法提供了一个访问和改变一个无序关系中的对象的方法。一般来说,这个关系应该是一个NSSet或者是一个NSMutableSet的实例对象。无论如何,通过实现这些访问器,任何类都可以像使用一个NSSet实例对象一样使用,可以被用于模型化关系和使用KVC。
获取无序访问器
无序访问器模式实现之后就可以把这个对多关系看成是一个NSSet的实例对象,比如-countOf< key>方法类似于 NSSet的 count方法; -enumeratorOf< key>类似于 NSSet的 objectEnumberator方法;-memberOf< key>相当于member方法。
- (NSUInteger)countOfTransactions {
return [self.transactions count];
}
- (NSEnumerator *)enumeratorOfTransactions {
return [self.transactions objectEnumerator];
}
- (Transaction *)memberOfTransactions:(Transaction *)anObject {
return [self.transactions member:anObject];
}
- (void)addTransactionsObject:(Transaction *)anObject {
[self.transactions addObject:anObject];
}
- (void)addTransactions:(NSSet *)manyObjects {
[self.transactions unionSet:manyObjects];
}
- (void)removeTransactionsObject:(Transaction *)anObject {
[self.transactions removeObject:anObject];
}
- (void)removeTransactions:(NSSet *)manyObjects {
[self.transactions minusSet:manyObjects];
}
- (void)intersectTransactions:(NSSet *)otherObjects {
return [self.transactions intersectSet:otherObjects];
}
键值校验
KVC提供了一系列的API来校验一个属性值。校验基础结构提供一个类接受一个值的机会,结果将返回另一个值,或者拒绝一个属性的新值并返回错误的理由。
校验方法命名规则
就好像访问器方法的命名规则一样,关于属性的校验方法也有命名规则。一个校验方法的规则应该是validata< key>:error:。列表1展示了name属性的校验方法的命名规则:
-(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {
// Implementation specific code.
return ...;
}
实现一个校验方法
校验方法通过引用传递连个参数:要校验的值对象和用于返回错误信息的NSError对象。
一个校验方法可能有三种输出:
1.值对象是有效的,不改变值对象和错误信息,并且返回YES
2.值对象是无效的,并且一个有效的值不能被创建和返回。结果在把错误设置到NSError参数中并且返回NO以表示校验的结果是失败。
3.一个新的有效的值对象被创建和返回。这种情况下,将参数设置成一个新的有效值,然后返回YES。返回的错误是不变的。你必须返回一个新的对象,而不是仅仅修改传递的ioValue,即使它是可变的。
-(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;
}
重要:一个返回NO的校验方法必须先检查outError参数是否为空。如果outError参数不是空的,那么校验方法应该设置outError参数为一个有效的NSError对象。
调用校验方法
你可以直接调用校验方法,也可以通过调用 -validataValue:forKey:error和一个确切的key来调用校验方法。默认的实现方法 -validateValue:forKey:error 在类中搜索那些名字和validata< key>:error相匹配的receiver。如果确实有这样的一个方法,那么它将会被引用和作为结果返回 。如果没有这样的方法,validataValue:forkey:error返回YES,然后校验这个值。
警告:当你为一个属性实现方法-set< key>时,不应该调用校验方法
自动校验
通常来说,KVC不会自动校验,引用校验方法是你的app的责任
有些技术在某些环境下回自动地进行校验:Core Data当对象内容被保存时会自动地执行校验方法。在OS X中,Cocoa绑定允许你指定自动进行校验的方法。(间Cocoa绑定编程指南以查看更多)
标量的校验
校验方法要求参数值是一个对象,作为结果值返回的非对象属性值会被包装成NSValue或者NSNumber类,参见标量和结构体支持文。下面是例子。
-(BOOL)validateAge:(id *)ioValue error:(NSError * __autoreleasing *)outError {
if (*ioValue == nil) {
// Trap this in setNilValueForKey.
// An alternative might be to create new NSNumber with value 0 here.
return YES;
}
if ([*ioValue floatValue] <= 0.0) {
if (outError != NULL) {
NSString *errorString = NSLocalizedStringFromTable(
@"Age must be greater than zero", @"Person",
@"validation: zero age error");
NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorString };
NSError *error = [[NSError alloc] initWithDomain:PERSON_ERROR_DOMAIN
code:PERSON_INVALID_AGE_CODE
userInfo:userInfoDict];
*outError = error;
}
return NO;
}
else {
return YES;
}
// ...
确保KVC遵从
为了使你的类的特定属性符合KVC规则,你必须为这些属性实现方法valueForKey:和setValue:forKey:。
属性和对一关系的遵从
如果一些属性是attribute或者对一关系,则下面这些方法必须实现:
- 实现以 -< Key>, -is< key>或者是 < key>或者 _< key>命名的实例对象和方法.尽管key频繁地以一个小写字母开头,KVC也支持以一个大写字母开头,比如URL。
- 如果一个属性是可变的,那么你也应该实现方法-set< key>:。
- 当你实现-set< key>时不能调用校验方法。
- 如果需要校验方法是你的类应该实现方法 -validate< key>:error:。
索引对多关系的遵从
在索引对多关系中,KVC要求你实现下面的方法:
- 实现以-< key>命名并且返回一个数组的方法或者有数组实例以< key>或者_< key>命名。
- 实现方法-countOf< key>和 -objectIn< key>AtIndex:或者-< key>AtIndexes:的其中一个。
- 为了提高性能,你可以实现-get< key>:range:方法。
对于一个可变的排序的索引对多关系,KVC要求你实现下面的方法:
- -insertObject:in< Key>AtIndex: 或者 -insert< Key>:atIndexes:其中一个
- -removeObjectFrom< Key>AtIndex: or -remove< Key>AtIndexes:其中一个
- -replaceObjectIn< Key>AtIndex:withObject: 或者 -replace< Key>AtIndexes:with< Key>: 来提高性能
无序对多关系的遵从
在无序对多关系中,KVC要求你实现下面的方法:
- 实现以-< key>命名并且返回一个数组的方法或者有数组实例以< key>或者_< key>命名。
- 实现方法-countOf< key>和 -enumeratorOf< Key>, 和 -memberOf< Key>:。
对于一个可变的无序对多关系,KVC要求你实现下面的方法:
- -add< Key>Object: 或者 -add< Key>:其中一个
- -remove< Key>Object: or -remove< Key>:其中一个
- -intersect< Key>: and -set< Key>: 你可以选择实现这两个方法来提高性能
标量和结构体支持
KVC通过自动地把标量值和数据结构体封装和解包成NSNumber和NSValue类型的实例数据来实现对标量和结构体的支持。
描述非对象值
默认的实现方法valueForkey:和setValue:forkey:提供了对把非对象数据类型,包括标量和结构体封装成对象的支持。
一旦valueForKey:方法在确切的访问器方法或者实例对象值中被定义,它检查返回类型或者数据类型,如果只的返回类型不是一个对象,就创建一个NSNumber或者NSValue对象来代替这些非对象数据类型。
同样的,setValue:forKey:一档在访问器方法或者实例对象中被定义,如果数据类型不是一个对象,那么程序将会调用从-< type>value方法中返回的对象作为返回对象。
处理nil值
当setValue:forkey:传入一个空值作为非对象属性的参数时,一个额外的问题会出现,并且它没有相应的处理方法。当非对象属性的值为空时,应该调用setNilValueForKey:方法。默认的setNilValueForKey方法会抛出一个NSInvalidArgumentException异常。一个子类可以重载这个方法来处理相应的行为。例子参见下面代码
- (void)setNilValueForKey:(NSString *)theKey
{
if ([theKey isEqualToString:@"age"]) {
[self setValue:[NSNumber numberWithFloat:0.0] forKey:@”age”];
} else
[super setNilValueForKey:theKey];
}
封装和解包标量类型
Data type | Creation method | Accessor method |
---|---|---|
BOOL | numberWithBool: | boolValue |
char | numberWithChar: | charValue |
double | numberWithDouble: | doubleValue |
float | numberWithFloat: | floatValue |
int | numberWithInt: | intValue |
long | numberWithLong: | longValue |
long long | numberWithLongLong: | longLongValue |
short | numberWithShort: | shortValue |
unsigned char | numberWithUnsignedChar: | unsignedChar |
unsigned int | numberWithUnsignedInt: | unsignedInt |
unsigned long | numberWithUnsignedLong: | unsignedLong |
unsigned long long | numberWithUnsignedLongLong: | unsignedLongLong |
unsigned short | numberWithUnsignedShort: | unsignedShort |
封装和解包结构体
Data type | Creation method | Accessor method |
---|---|---|
NSPoint | valueWithPoint: | pointValue |
NSRange | valueWithRange: | rangeValue |
NSRect | valueWithRect: (OS X only). | rectValue |
NSSize | valueWithSize: | sizeValue |
自动封装和解包不限制与NSPoint,NSRange,NSRect和NSSize结构体类型。例如,你可以像下下面这样定义一个类:
typedef struct {
float x, y, z;
} ThreeFloats;
@interface MyClass
- (void)setThreeFloats:(ThreeFloats)threeFloats;
- (ThreeFloats)threeFloats;
@end
-mark
通过使用参数@”threeFloats”调用valueForkey方法发送MyClass的实例对象的消息将会调用MyClass方法threeFloats,然后结果将包装成为一个NSValue类型。除此之外向用一个ThreeFloats结构体包装成的NSValue对象向一个MyClass的实例对象发送一个setValue:forKey:的消息,将会调用setThreeFloats方法,然后结果将会得到getValue:message的结果。
Collection操作
例子中的数据使用
简单的Collection操作
数组和集合操作
访问器搜索的实现细节
简单属性的访问器搜索模式
有序collection的访问器搜索模式
有序非重复collection的访问器搜索模式
无序collection的访问器搜索模式
—–以上坑待填
描述属性关系
在一个类中,类描述提供了一种方法去描述对一和对多关系、 在使用KVC后,定义这些类属性间的关系允许对这些属性进行更智能和灵活的操作。
类描述
NSClassDescription是一个提供了获取类的元数据接口的基础类。一个类描述对象在一个个类的对象和其他对象间,记录了一个特定的类的对象的可获取属性和关系(一对一,一对多和互斥)。例如属性方法将返回一个类定义的所有属性。方法toManyRelationshipKeys和toOneRelationshipKey:返回定义了对多和对一关系的keys的数组,inverseRelationshipKey:根据提供的keys返回从关系的终点指向回的关系的名字。
NSClassDescription 不定义用于定义关系的方法,具体的方法由子类来实现。一旦创建成功,你就可以通过NSClassDescription里的 registerClassDescription:forClass:类方法来注册你的类。
NSScriptClassDescription 是Cocoa里提供的唯一的NSClassDescription的实例子类。它封装了一个app的脚本的信息。
性能考虑
尽管KVC是非常有效率的,但是它添加了一个比直接调用的方法级别更加低的间接寻址方法。所以只有当你能够通过它将程序变得更加灵活的时候你才应该使用它。可能在将来,我们会添加额外的优化,但是这些优化并不会改变KVC遵循最基本的方法。
重载KVC方法
默认的时间KVC的方法,比如valueForKey: 缓存OC运行信息来增加效率。当你重载这些方法的时候,你应该注意确保你的重载方法不会影响app的性能。
优化对多关系
在使用索引格式的访问器中的对多关系实现将会造成比较多的性能消耗。
建议你尽可能地在对多关系集合中使用最少的索引访问器。参见Collection Accessor Patterns for To-Many Properties以查看更多
Introduction to Key-Value Observeing Programming Guide
关于KVO模式的介绍
resource:
Key-value observing (键值观察模式)是一个对象的值发生变化时,将得到注意、通知的一个机制
在了解KVO之前,必须先学习Key-value coding,主要内容在上面。
一言蔽之
KVO提供了一种当某些确切的属性发生变化的时候,系统将得到通知的机制。在一个app中,这样的机制在模型和布局控制器发生交流的时候就显得十分的有用。(在OS X中,布局控制器绑定技术非常依赖KVO。)一个控制器对象一般观察模型对象的属性值,一个视图对象通过一个控制器来观察模型对象的属性值。另外,一个模型对象可能会观察其他模型对象(通常是决定当一个依赖的值发生改变时,它是否要随之改变)或者甚至它自己(当一个依赖的值发生改变它是否改变)。
你可以观察的属性值应该包括简单的属性,对一关系和对多关系。对多关系的观察者将会被告知改变的类型和变化中涉及到改变的对象。
要设置一个属性的观察者有三个步骤,明白这三个步骤就可以很清楚地说明KVO是怎么样工作的。
1.首先,看你键值观察的对象是否有直接的相关,比如,当有一个对象的一个特定的属性发生了任何改变时,与之相关的对象应该得到通知。
例如,一个personobject类应该注意任何他们在bankobject类中的accountbalance属性发生的任何改变
2.personobject类必须通过发送一个 addObserver:forKeyPath:options:context:消息来注册成为一个bankobject类的accountbalance属性的观察者
注意:方法 addObserver:forKeyPath:options:context: 建立了一个你确定的实例对象之间的链接。两个类之间是不能建立连接的,但是两个实例对象可以。
3.为了相应改变通知,观察者必须实现observerValueForKeyPath:ofObject:change:context:方法。这个方法的实现定义了观察者怎样相应这些改变通知。在这个方法里面你可以定制对其中一个改变的属性的相应行为。
Registering for Key-Value Observing描述了怎么样注册和接受观察通知。
4.方法 observeValueForKeyPath:ofObject:change:context:当观察的属性值以KVO兼容的方式改变时,或者它指向的key发生改变时,这个方法就会自动调用。
Registering Dependent Keys确定一个值的key取决于另一个key的值
KVO的最基本的有点就是每次当一个属性发生变化时,你不需要实现发送通知的流程。它已经定义好的基层有着框架级别的支持,可以让更加容易地实现。一般来说你不需要在你的工程中添加额外的代码。另外,这个基层已经十分的完善了,可以支持对一个单一属性或者以来至添加多个观察者。
KVO Compliance 描述了自动和手动键值观察的不同,和如何同时实现
不像通知中心模式使用NSNotificationCenter,对于所有的观察者并没有中央对象来提供改变通知。通知将直接从发生改变的对象发送的观察者对象。NSObject实现了最基础的KVO,你基本不需要去重载这些方法。
Key-Value Observing Implementation Details描述了KVO模式是怎么样实现的。
KVC实现
为了使一个特定的属性遵从KVO,这个类必须确保下面几点:
- 这个类的属性必须是KVC实现的
- 这个类为属性发出KVO改变通知
- 已经注册过依赖key(详见Registering Dependent Keys)
有两个技术确保了改变通知的发出。由NSObject提供的自动支持和一个类中所有属性默认可用的KVC实现。一般来说,你遵守标准的Cocoa编码和命名规范,你就可以使用自动改变通知,你并不需要写额外的代码。
手动改变通知当通知发出时提供了额外的控制,并且需要额外的编码。你可以通过实现类方法automaticallyNotifiesObserversForKey:方法在你的子类的属性控制自动通知。
自动改变的通知
NSObject提供了一个基本的自动键值改变通知的实现。自动键值改变通知告知观察者那些键值实现和KVC方法的访问器所作出的改变。自动通知也通过collection代理对象返回被支持,比如mutaleArrayValueForKey:。参照下面代码:
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
手动改变的通知
手动改变通知通过怎么样的和什么时候的通知被发送到观察者提供了更加细致的控制。这可以帮助尽可能少地触发一些不需要的通知,或者把一组改变变成一个单一的通知。
一个要实现手动通知的类必须重载NSObject类中的automaticallyNotifiesObserversForKeys:方法。在一个类中同时使用自动和手动的通知也是有可能的。属性执行手动通知,子类实现automaticallyNotifiesObserversForKey:应该返回NO,一个子类的实现应该调用父类包括父类中的所有未识别的keys(意思就是这个子类中没有这个key时,应该在父类中找是否有这个key?)。参照下面代码:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"openingBalance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
为了实现手动观察者通知,你应该在改变值前调用willChangeValueForKey:,在改变值后调用didChangeValueForKey:。参见下面代码:
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
你可以通过先检查值是否发生改变来尽可能地减少发送一些不必要的通知。参见下面代码:
- (void)setOpeningBalance:(double)theBalance {
if (theBalance != _openingBalance) {
[self willChangeValueForKey:@"openingBalance"];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
}
当一个简单的改变会引起多个keys发生改变时,你应该把这些改变通知合并。
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
[self willChangeValueForKey:@"itemChanged"];
_openingBalance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"openingBalance"];
}
涉及到有序对多关系时,你应该考虑的不仅是key的改变,还应该包括索引的对象的改变那和类型的改变。类型的改变是一个包含了 NSKeyValueChangeInsertion, NSKeyValueChangeRemoval,或者NSKeyValueChangeReplacement的NSKeyValueChange对象,索引对象则是传递一个NSIndexSet对象。
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}
注册依赖keys
一个属性的值由另一个对象的一个或者多个属性决定有许多情况。如果一个属性的值改变,这个值的派生属性应该也是被标记被改变。你如何确保这些依赖项属性值监听通知张贴取决于关系的基数。
对一关系
为了在对一关系中自动触发通知你应该重载keyPathForValueAffectingValueForKey:或者定义一个注册为依赖值的适合的方法模式。
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
一个观察fullname属性的app应该在firstname或者lastname属性改变或者他们属性的值发生改变的时候被通知。
一种解决方案就是重载keyPathForValueAffectingValueForKey:指定fullname属性是基于lastname和firstname属性的一个人的属性。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
你的重载方法一般应该调用父类并且放回一个包括了所有成员的父类。
你也可以通过实现一个叫做keyPathsForValuesAffecting< key>名字的类方法来达到这个目的。
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
当你往一个已经存在的使用category的类中添加一个计算出来的属性时,你不能重载keyPathsForValueAffectingValueForKey方法,因为你不应该在categories里重载方法。在这个例子中,实现keyPathsForValuesAffecting< key>类方法更有优势。
注意:你不能通过实现keyPathsForValuesAffectingValueForKey:方法在对多关系中设置依赖值。你应该观察适当的对多关系集合中的每个对象中的适当的属性,然后根据这些值的改变更新他们的key。接下来这一章展示了这种情况的处理方法。
对多关系
keyPathsForValuesAffectingValueForKey:方法不支持对多关系的key-paths。例如,你对于一个employee类有一个有对多关系的Department对象,并且employee有一个工资属性。那么你应该想department对象有一个totalsalary属性,可以把所有的职员的工资自动加起来。那么你就不能,比如使用keyPathForValuesAffectingTotalSalary和把employees.salary作为key返回。
两面有两种解决这种情况的方法:
1.你可以使用KVO注册父类为一个他们所有子类的相关属性的观察者。你应该随着子类的添加和删除添加和删除父类的观察者。在observeValueForKeyPath:ofObject:change:context:方法中更新你的依赖值,下面代码是示例:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
如果你使用Core Data,你应该在app通知中心中把父类注册为它管理的对象内容观察者。当子类发生变化时,父类应该像键值观察模式一样该得到相关的通知。
KVO的实现细节
自动键值观察是通过一种叫isa-swizzling技术实现的。
isa指针,正如它的名字所示,指向了一个维护调度表的一个类。这个调度表本质上包括了类实现的方法和其他数据的所有指针。
当一个观察者为一个对象的一个属性被注册时,观察对象的isa指针被修改,指向一个中间类而不是原来的那个真实的类。所以isa指针的值不一定指向实例对象的那个类。你不能通过isa指针来决定类成员,你应该使用实例对象的类方法来决定类。