KVC 规范
参考文档:Adopting Key-Value Coding
基础KVC规范
当对对象使用KVO时,我们通过让对象继承NSObject或其它子类,来依靠NSKeyValueCoding
协议的默认实现。相应的,默认实现也依赖于你的对象实例变量和访问器方法遵循定义明确的模式,以便你在收到valueForKey:
或者setValueForKey:
时可以正确的将key string和属性关联起来
我们通常使用@property
来声明属性,并允许编译器自动生成实例变量和访问器方法。 如果需要在Object-C中手动实现访问器,请遵循下面所说的规范。如果要提供其他功能来增强与任何语言的对象的集合属性的交互,请实现Defining Collection Methods。如果要通过键值验证来进一步加强对象,请实现Adding Validation描述的方法
基础Getters
- (NSString*)title
{
// Extra getter logic…
return _title;
}
//对于bool值属性 可以使用前缀is
- (BOOL)isHidden
{
// Extra getter logic…
return _hidden;
}
基础setter
- (void)setHidden:(BOOL)hidden
{
// Extra setter logic…
_hidden = hidden;
}
注意
永远不要在set
:方法内部调用验证属性方法
实例变量
当KVO找不到属性的访问器时,会查询accessInstanceVariablesDirectly:
类方法是否允许直接使用实例变量。
如果允许的话,请确保以常规方式命名,使用带下划线的属性名作为实例变量名,通常编译器帮我们执行该操作,也可以使用@synthesize
自定义命名
@synthesize title = _title;
某些情况下,可以使用@dynamic
指令来通知编译器我们将在运行时提供getter和setter方法,而不是使用@synthesize
指令或者允许编译器自动合成属性。这样做是为了避免自动合成自动合成gettre方法,从而可以像定义集合方法所属,提供集合访问器。这种情况下,需要自己将实例变量声明为接口的一部分
@interface MyObject : NSObject {
NSString* _title;
}
@property (nonatomic) NSString* title;
@end
定义集合方法
对于使用标准命名创建的访问器或者实例变量,KVC可以正确找到它们,对于表示对多关系的集合对象或其他属性来说也是如此。但是,如果代替集合属性的基本访问器方法或者在集合属性的基础上实现及和访问器方法,就可以:
- 在NSArray和NSSet之外类实现对多关系模型。当在对象中实现了集合方法,kvo getter默认会返回一个代理对象,此代理对象调用这些方法来响应收到的
NSArray或NSSet
消息。代理对象并不一定为NSArray或者NSSet本身,因为代理对象提供了对集合方法的预期正确行为 - 更改对多关系内容是,提高性能。代替对基本的setter重复创建新的集合对线来响应每次更改,而是使用集合方法来对基础属性进行适当的突变
- 提供KVO合规性来访问对象的集合属性内容。
根据希望集合表现为索引排序、有序集合(比如NSArray)、还是无序唯一集合(例如NSSet),来实现两种类型的集合访问器之一。无论哪种情况,都需要至少一组方法来支持对属性的读取访问,以及另一组启用对集合内容的更改
访问索引集合
可以添加索引访问器方法,对有序关系中的对象进行计数、检索、添加和替换。
索引集合Getter
对于没有默认getter方法的属性,如果提供以下索引结合getter方法,则将响应valueForKey
方法,返回类似NSArray的代理对象
注意
在OC中,编译器会默认为属性合成getter方法。可以通过不声明属性或者将其声明为
@dynamic
来解决
countOf<Key>
类似NSArray计数方法,必须实现的
- (NSUInteger)countOfTransactions { return [self.transactions count];
}objectIn<Key>AtIndex: or <key>AtIndexes:
第一个返回对象在对多关系指定索引处对象,第二个返回在NSIndexSet参数指定索引处对象数组。对应NSArray的objectAtIndex:和objectsAtIndexes:
。只需要实现两者之一即可- (id)objectInTransactionsAtIndex:(NSUInteger)index { return [self.transactions objectAtIndex:index];
}
- (NSArray *)transactionsAtIndexes:(NSIndexSet *)indexes {
return [self.transactions objectsAtIndexes:indexes];
}get<Key>:range:
可选方法,但是可以提到性能。从集合中返回指定范围的对象,与NSArray的getObjects:range:
相对应- (void)getTransactions:(Transaction * __unsafe_unretained *)buffer range:(NSRange)inRange {
[self.transactions getObjects:buffer range:inRange];
}
可变索引集合
可以实现另外一组方法来实现索引集合可变的对多关系。此时响应mutableArrayValueForKey:
方法。默认返回一个类似NSMUtableArray
的代理对象,其使对多关系内容符合KVO
insertObject:in<Key>AtIndex: or insert<Key>:atIndexes:
- (void)insertObject:(Transaction *)transaction inTransactionsAtIndex:(NSUInteger)index {
[self.transactions insertObject:transaction atIndex:index];
}
- (void)insertTransactions:(NSArray *)transactionArray
atIndexes:(NSIndexSet *)indexes {
[self.transactions insertObjects:transactionArray atIndexes:indexes];
}removeObjectFrom<Key>AtIndex: or remove<Key>AtIndexes:
仅需要实现其一即可
- (void)removeObjectFromTransactionsAtIndex:(NSUInteger)index { [self.transactions removeObjectAtIndex:index];
}
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self.transactions removeObjectsAtIndexes:indexes];
}replaceObjectIn<Key>AtIndex:withObject: or replace<Key>AtIndexes:with<Key>:
提供了直接替换方法,而不需要先删除再插入。可选的,但是可以用于提高性能。
- (void)replaceObjectInTransactionsAtIndex:(NSUInteger)index withObject:(id)anObject {
[self.transactions replaceObjectAtIndex:index
withObject:anObject];
}
- (void)replaceTransactionsAtIndexes:(NSIndexSet *)indexes
withTransactions:(NSArray *)transactionArray {
[self.transactions replaceObjectsAtIndexes:indexes
withObjects:transactionArray];
}
无序集合
用于访问无序关系对象,此关系是NSSet
和NSMutableSet
对象实例。
无序集合getter
提供以下的集合getter方法时,遍历集合对象,并测试对象是否在集合中,来响应valueForKey:
消息。返回类似NSSet对象。
countOf<Key>
- (NSUInteger)countOfEmployees { return [self.employees count];
}enumeratorOf<Key>
此方法返回一个NSEnumerator
实例,用于遍历关系中的项目。
有关枚举器信息参见 Enumeration: Traversing a Collection’s Elements- (NSEnumerator *)enumeratorOfEmployees { return [self.employees objectEnumerator];
}memberOf<Key>:.
将参数传递的对象和集合内容进行比较,返回匹配的对象作为结果;未找到则返回nil。
- (Employee *)memberOfEmployees:(Employee *)anObject { return [self.employees member:anObject];
}
可变的无序集合
实现可变的无序访问器,这将允许对象响应mutableSetValueForKey:
方法,返回武学的集合代理对象。实现这些访问器比依赖访问器直接返回可变对象以对关系中的数据进行更改要高效的多。 也可以使类符合KVO
add<Key>Object: or add<Key>:
类似NSMutableSet的
addObject:
和unionSet:
方法- (void)addEmployeesObject:(Employee *)anObject { [self.employees addObject:anObject];
}
- (void)addEmployees:(NSSet *)manyObjects {
[self.employees unionSet:manyObjects];
}remove<Key>Object: or remove<Key>:
它们类似于NSMutableSet方法
removeObject:和minusSet:
。实现二者其一即可- (void)removeEmployeesObject:(Employee *)anObject { [self.employees removeObject:anObject];
}
- (void)removeEmployees:(NSSet *)manyObjects {
[self.employees minusSet:manyObjects];
}intersect<Key>:
接收NSSet参数,从关系中删除输入集合和集合中部共有的对象。相当于
NSMutableSet
的intersectSet:
方法- (void)intersectEmployees:(NSSet *)otherObjects { return [self.employees intersectSet:otherObjects];
}
处理非对象值
一般对于非对象值,依赖KVC中的默认实现来对其进行对象的包装和解包。但是可以覆盖默认行为,最常见的原因是处理非对象属性上对nil的处理
如果KVC对象符合键值编码对象收到了setValue:forKey:
消息,并且将nil作为非对象属性的值时,默认实现并没有合适的措施。因此,它向自己发送了setNilValueForKey:
消息,我们可以覆盖此消息。
setNilValueForKey:
的默认实现会触发NSInvalidArgumentException
异常,我们可以提供适当的特定实现的行为
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:@"age"]) {
[self setValue:@(0) forKey:@”age”];
} else {
[super setNilValueForKey:key];
}
}
注意
为了向后兼容 当对象覆盖已经弃用的
notableToSetNilForKey:
方法时,setValue:forKey:
会调用该方法而不是setNilValueForKey:
添加验证
在KVC协议中 定义了通过key或者keyPath验证属性的方法。具体来说,为要验证的key和属性提供validate<Key>:error:
方法
假如没有为属性提供验证方法,协议的默认实现会认为属性验证成功而不论值为多少,这意味你需要逐个属性进行验证。
实现验证方法
为属性提供验证方法时,该方法通过引用接收两个参数:要验证的对象值 以及 用于返回错误信息的NSError。因此验证可能会有三种不同的结果:
- 验证方法觉得值对象是有效的,并且在不更改值和错误参数的情况下返回YES
- 验证方法认为值对象无效,但是选择并不对其进行更改,这种情况下,该方法返回NO并将错误引用设置为只是错误原因的NSError对象
- 验证方法任务值对象无效,但是创建了一个新的有效对象作为替换。此时,该方法返回YES,同时保持错误对象不变。在返回之前,该方法将值指向新的值对象。进行修改时,即使值对象是可变的,该方法也会创建一个新对象,而不是修改原有对象
- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
if (outError != NULL) {
*outError = [NSError errorWithDomain:PersonErrorDomain
code:PersonInvalidNameCode
userInfo:@{ NSLocalizedDescriptionKey
: @"Name too short" }];
}
return NO;
}
return YES;
}
验证标量值
验证方法中期望value参数是一个对象,而非对象属性值包装在NSValue和NSNumber
中。
- (BOOL)validateAge:(id *)ioValue error:(NSError * __autoreleasing *)outError {
if (*ioValue == nil) {
// Value is nil: Might also handle in setNilValueForKey
*ioValue = @(0);
} else if ([*ioValue floatValue] < 0.0) {
if (outError != NULL) {
*outError = [NSError errorWithDomain:PersonErrorDomain
code:PersonInvalidAgeCode
userInfo:@{ NSLocalizedDescriptionKey
: @"Age cannot be negative" }];
}
return NO;
}
return YES;
}
属性关系描述
类描述提供了一种描述类中一对一和一对多属性的方法。定义类属性之间的关系可以通过KVC对这些属性进行更智能、更灵活的操作
类描述
NSClassDecription
是一个基类,提供用于获取有关类的元数据的接口。类描述对象记录特定类的对象的可用属性,以及该类对象和其他对象之间的关系。例如:attributeKeys
方法返回一个类定义的所有属性列表。ToManyRelationShipKeys
和toOneRelationShipKeys
返回定义一对多和一对一的key数组。inverseRelationShipKey:
返回 从提供的key的关系的目标指向接受者的关系的名称
NSClassDescription没有定义用于定义关系的方法。具体的子类必须定义这些方法。创建后,您可以使用NSClassDescriptionregisterClassDescription:forClass:
类方法注册类描述。
NSScriptClassDescription是Cocoa中提供的NSClassDescription的唯一具体子类。它封装了应用程序的脚本信息。
性能设计
KVC合规
一对一关系属性合规
对于作为属性或一对一关系的每个属性:
- 实现名为
<key>
或者is<Key>
方法,创建实例变量为key
或_key
。编译器通常会在自动合成属性时执行此操作 - 如果属性是可变的,实现
set<Key>:
方法,编译器通常在自动合成属性时 执行此操作 - 如果属性是标量,重写
setNilValuForKey:
方法处理nil分配给标量情况
对多属性索引合规
对于一对多关系属性:
- 实现一个名为
的方法返回数组。或者名为 <_key>
和<key>
数组实例变量。编译器通常在自动合成属性时 执行此操作 - 或者实现
countOf<Key>
以及objectIn<Key>AtIndex:
和<key>AtIndexes
之一即可 - 可选的实现
get<Key>:range:
提高性能
除此之外,如果属性是可变的 - 实现一个或两个方法insertObject:in
AtIndex:并插入Key:atIndexes:。 - 实现一个或两个方法removeObjectFrom
AtIndex:和remove Key AtIndexes:。 - (可选)实现replaceObjectIn Key AtIndex:withObject:或用Key替换Key AtIndexes:with来提高性能
无序对多关系合规
对于是无序对多关系的每个属性(例如NSSet对象):
实现返回一个集合的键,或者实现一个名为key或_ key的NSSet实例变量。编译器通常会在自动合成属性时为您执行此操作。
或者,实现countOf Key,enumeratorOf Key和memberOf Key:方法。
另外,如果属性是可变的:
实现一个或两个方法添加Key Object:和add Key:。
实现一个或两个方法来删除Key Object:并删除Key:。
(可选)实现相交键:以提高性能
验证
选择验证需要的属性:
实现validate Key:error:
方法,返回指示值有效性的布尔值,并在适当时返回对错误对象的引用。