什么是键值编码
什么是键值编码
访问实例变量可以通过访问器访问,也可以将属性设置为@public。
与此相对,键值编码(key-value coding)是指,将表示对象包含的信息的字符串作为键值使用,来间接访问该信息的方式。键值编码提供了非常强大的功能,基本上,只要存在访问器方法,声明属性或实例变量,就可以将其名字指定为字符串来访问。本章中,可以访问,设定的对象状态的值称为属性(property)。
之所以说键值编码的访问是间接的,是因为以下两点:
也可以在运行中确定作为键的字符串。
使用者无法知道实际访问属性的方法。
键值编码的基本处理
键值编码的必须方法在非正式协议NSKeyValueCoding中声明,这些默认在NSObject中实现。
有访问器的属性会使用该访问器,没有访问器的属性也可以设定值和访问。而且变量obj也可以为id类型。
访问属性
键值编码的方法的行为
下面的讲述围绕name进行。以下划线开始的名字不能用于方法名或实例变量名。此外,也不要使用以get开始的名字。
接收器中如果有name访问器则使用它。
没有访问器时,使用接收器的类方法accessInstanceVariablesDirectly来查询。返回YES时,如果存在实例变量name时(或_name,isName,_isName等)则返回其值。使用引用计数的方式时,实例变量如果是对象,则旧值会被自动释放,新值被保存并带入。
既没有访问器也没有实例变量时,将引起接收器调用方法setValue:forUndefinedKey:
应该返回的值如果不是对象,则返回被适当的对象包装的值。
但是,如果接收器包含与带索引的访问器模式一致的方法,则将返回有数组对象行为代理(proxy)对象。
accessInstanceVariablesDirectly只要该方法返回YES,实例变量的可见属性即使有@private修饰,也可以访问。
setValue:forKey:如果设定值失败,则调用下面的方法。
- (void)setValue:(id)value forUndefinedKey:(nonnullNSString *)key
//不能设置键字符串key对应的属性值时,从方法setValue:forKey中调用该方法。默认情况下,该方法的执行会触发异常NSUndefinedKeyException。不过,通过在子类中修改定义,就可以返回其他对象。
键值编码比访问器以及实例变量名更具灵活性。
由于键值编码所接收的对象都是id类型,因此,在该部分中,编译时不会进行仔细的类型检查。所以一定要注意不要传入与属性不符的对象。
使用键值编码的程序如何执行,不是由静态解析源码语法的结果决定的,而是使用程序运行时包含的信息动态决定的。与实例变量的可视属性不同,有方法决定是否可以访问实例变量这一点,会使人感觉有损一致性。总之,键值编码这一强大的功能就像一把双刃剑,也伴随着危险,因此不可以滥用。
属性值的自动转换
将属性中单纯的数值,也就是整数或实数,布尔值这样的数据称为标量(scalar)值。将标量值,结构体,字符串或NSNumber等常数对象称为属性(attribute)。
方法setValue:在返回值为标量值或结构体时,会返回将其自动包装的对象。另一方面,为了给setValue:forKey:传入值,也需要使用适当的对象来包装。
单纯的数值用NSNumber来包装,结构体用NSValue类的实例来包装。属性值如果为对象,则可以将nil作为值传递。另一方面,当将nil作为值传递时,setNilValueForKey:方法将被发送给接收器。
- (void)setNilValueForKey:(NSString *)key
//执行该方法将产生NSInvalidArgumentException异常
字典对象和键值编码
字典类NSDictionary和NSMutableDictionary包含了协议NSKeyValueCoding的方法,使用它们可以进行键值编码。
- (id)valueForKey:(NSString *)key
//键字符串开头不是@时,将调用方法objectForKey:。如果开头为”@“,则将去除开头字符后剩余的字符串作为键,调用超类的方法valueForKey:。
NSMutableDictionary中定义了以下的方法:
- (void)setValue:(id)value forKey:(NSString *)key
//一般会调用方法setObject:forKey:,参数value为nil时,调用方法removeObjectForKey:删除键对应的对象。
根据键路径进行访问
属性为对象时,该对象还可能持有属性。在键值编码中,使用某个键访问获得某个属性对象后,如果希望再用别的键来访问该对象,可采用如下方法:
idname = [aGroup valueForKey:@“leader.name"];
像这样,用“.”连接键表示的字符串称为键路径(key path)。只要能找到对象,点和键多长都没有关系。
声明属性的点是运算符,而这里的键路径则是一个字符串。
使用键路径访问属性的方法如下:(略)
一对一关系和一对多关系
使用键(或键路径)访问时,我们将对象确定为一个的属性称为指定一对一关系(to-one relationship)的属性,将属性值为数组或集合的属性称为指定一对多关系(to-many relationship)的属性。
如果键对应一个对象,那么也是一对一关系。
关于一对多关系属性的访问,更改,需要留意以下几点:
1.使用集合元素对象持有的键访问一对多关系属性时,键对应的属性被作为数组或集合返回。
2.使用集合元素对象持有的键设定一对多关系属性时,各元素对象键对应的属性全都被更改。
数组对象和键值编码
数组类NSArray和NSMutableArray以及集合类NSSet和NSMutableSet都包含协议NSKeyValueCoding的方法,也都有键值编码。
- (id)valueForKey:(NSString *)key//以key为参数,对集合的各元素调用方法valueForKey:后返回数组(NSSet时返回集合)。对各成员适用方法valueForKey:,返回nil时,则包含NSNull实例。
- (void)setValue:(id)value forKey:(NSString *)key//对集合各元素调用方法setValue:forKey:。需要注意的是,即使集合对象自身不可以改变,也能调用该方法。
一对多关系的访问
带索引的访问器模式
即使是非数组对象,如果有某个模式的访问器,也可以进行像数组一样的键值编码操纵。该访问器模式称为带索引的访问器模式(indexed accessor pattern)。
下面是两个方法等实现。下划线部分会输入字符串。
- (NSUInteger)countOf___;
- (id)objectIn___AtIndex:(NSUInteger)index;
为了提高运行效率,除上述两个方法外,还可以实现下面的方法。这也是和数组类簇中的getObjects:range:相同的方法。
- (void)get___:(id__unsafe_unretained[])aBuffer range:(NSRange)aRange;
一对多关系的可变访问
获得可变数组对象的方法。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key
//返回相当于用键字符串指定的一对多关系的属性的可变数组。操作被返回的数组与操作属性同时进行。
- (NSMutableArray *)NSMutableArrayValueForKeyPath:(NSString *)keyPath
//接收器属性在键路径中指定。
用该方法操作属性时,除了之前提到的访问常数数组需要添加的两个方法之外,还需要实现用来插入和删除的方法。下划线部分加入了键字符串(通常为复数形式)。使用这些方法并通过键值编码访问时,内部的代理就会作为数组进行操作了。
- (void)insertObject:(id)obj in___AtIndex:(NSUInteger)index;
- (void)removeObjectFrom___AtIndex:(NSUInteger)index;
不仅是上述两个方法,通过实现下面的方法也可以实现属性的可变访问。
- (void)set___:(id)anArray;
在实现该方法时,通过使用被作为参数传入的数组元素对象,就可以置换一对多关系的全部属性内容。
在添加了插入和删除的方法的基础上,如果能实现下面的方法,则将能显著改善运行效果。
- (void)replaceObjectIn___AtIndex:(NSUInteger)index withObject:(id)obj;
如果上述方法都没有实现,那么当存在与键字符串同名的实例变量且该变量又是可变数组的对象时,方法mutableArrayValueForKey:将直接返回该值。
KVC标准
验证属性值
在某些情况下,如果预期之外的对象被设定了属性值,那么就可能出现问题。
因此,在为某属性带入对象前,可以使用相应的方法来验证,但是验证方法不能自动调用(使用Cocoa绑定时,可以设定自动验证),因此,在访问属性前,必须自行调用该方法。
验证某键字符串的属性值的方法可按如下形式定义。下划线中写入键字符串。参数ioValue为需要验证的对象的指针。参数outError被用来当验证结果中存在时返回出错信息。
- (BOOL)validate___:(inoutid*)ioValue error:(outNSError **)outError;
对象有问题时,但是能将对象修正为有效值时,方法会创建新的对象,并取代原对象将新对象带入ioValue。参数outError不变,返回值为YES。
对象有问题且不能修正时,则创建错误对象并将其带入参数outError。方法返回NO。
设定属性访问值的访问器方法(set___:)不能调用验证方法。
运行时,键会被动态地赋值给对象的情况下,不能在代码中使用上述方法名。此时,可以使用下面的两个方法。
- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString *)key error:(outNSError**)outError
//使用指定键寻找validate___:error:的验证方法并调用,如果不存在这样的验证方法,则返回YES。
- (BOOL)validateValue:(inoutid*)ioValue forKeyPath:(NSString *)keyPath error:(outNSError**)outError
//实际被调用的验证方法并不是该方法的接收器,而是与最后的键元素相对应的属性的验证方法。
键值编码的准则
如果可以使用键值编码来访问某个属性,则称该属性是键值编码的准则,或称为KVC准则(compliance)。反之,如果知道某属性为KVC准则,那么就可以编写使用键值编码的程序。KVC准则和协议适用的概念不同,它不是以类为单位,而是讨论以各个属性为单位是不是准则的问题。
要使某属性为KVC准则,就必须实现能使用valueForKey:方法的访问器。当属性可变时,还需要与方法setValue:forKey:相应的访问器。下面列举一些具体的条件:
property为属性(标量值或单纯型的对象)或一对一关系时,要想成为KVC准则,就需要满足如下条件。属性名为“name”。
1.实现了name或isName访问器方法。或者包含name(或_name)实例变量。
2.可变属性时,还需要实现setName:方法。需要执行键值验证时,要实现验证方法(validateName:error:)。但是,setName:方法中不能调用验证方法。
属性为一对多关系时,要想成为KVC准则。需满足如下条件。属性名为“names”。
1.实现了返回数组的names方法。或者持有包含names(或names)数组对象的实例变量或者实现了带索引的访问器模式的方法countOfNames以及objectInNamesAtIndex
2.当一对多关系的属性可变时
持有返回可变数组对象的names方法。或者
实现了带索引的访问器模式的方法
insertObject:inNamesAtIndex:以及removeObjectFromNamesAtIndex:。
当然,在实现带索引的访问器模式的方法时,为改善执行效率,也可以添加其他方法来实现。
键值观察
键值观察(key-value-observing),即某个对象的属性改变时通知其他对象的机制。有时也记作KVO。
对被视察对象来说,键值观察就是注册想要监视的属性的键路径和观察者。当属性改变时,观察者会接收到消息。
仅仅在使用键值编码准则来访问访问器或实例变量的情况下,才可以监视属性的变化。在方法内直接改变实例变量值时,就不能监视了。
NSObject中提供了键值观察所必需的方法,头文件Foundation/NSKeyValueObserving.h中将其定义为了非正式协议。
使用引用计数管理方式时需要注意一些事项。注册属性监视时,不需要持有观察者及监视对象的属性。还有,如果在不删除注册信息的情况下将关联对象释放,那么随着属性的变更,就可能会发生访问已释放了的对象的危险。
一对多的属性监视
监视方法与前述方法相同。但有一点必须注意,那就是一对多关系为数组类型时使用方法mutableArrayValueForKey:获得对象,集合类型同样如此,如果不修改值是不能被监视的。
依赖键的登记
某属性值伴随着同一对象的其他属性的改变而改变是常有的事情。通过事先将这样的依赖关系在类中注册,那么即使属性值间接的发生了变化,也会发送通知。为此,需要使用下面的类方法:(略)
Cocoa绑定描述
目标-行为-模式的弱点
在面向对象程序设计中,应尽可能地去除特定的类与类之间的关系,定义低耦合的类。也就是说,某个类改变时,最好不会影响其他的类,否则就不是我们期望的编程。但是,就算完美地定义了每个类,它们间如果不能联动也就不能实现功能。一个典型的例子就是,窗体及窗体上面的按钮菜单等GUI组件也是对象,它们间如果不连接,程序就不能运行。
这样看来,除了各个类或GUI组件原本就应该有的功能之外,还需要为它们之间的联动补充必要的代码。而这样的代码就像是把元素粘在一起的胶水,因此称为胶水代码(glue code)。胶水代码既可以被写成专门的类,也可以渗透在各个关联的类中。
Mac OSX从第一个版本NexTstep开始,就有了将GUI组件与使用组件的对象结合在一起的开发工具Interface Builder。“胶”的部分不需要特意编写代码,使用Interface Builder的的GUI环境就可以简单地实现,因此能大幅提高编程效率。Interface Builder中中GUI组件被操作时,会预先指定向哪个对象发送什么消息,这里,Objective-C灵活的消息发送机制发挥着非常大的作用。以上就是我们说明过的目标行为模式。
但是,“从操作组件向目标发送消息”这样的方式在很多情况下都是无能为力的。特别是当值改变时,为了使多个对象联动,必须书写专门的代码。图20-2(a)希望实现的是,绘图用的参数可从滑块或文本域输入中获得,并根据值的变化改变各种显示,这样的情况很常见,但只用Interface Builder连接解决不了这样的问题,还有必要使用专门的对象。程序自身虽然简单,但这样的组合多了的话,就要写大量相似的代码。
什么是Cocoa绑定
从Mac OS X 10.3起开始引入的Cocoa绑定(Cocoa binding)是指,使用键值编码和键值观察的组合,在多个对象间共享属性值的变化的机制。(在iOS中不可以使用)
上图(b)为例说明了它的概要。
Cocoa绑定所需的方法
使用Cocoa绑定,将某对象绑定到控制器属性时,该对象必须实现下面的方法。该方法用头文件AppKit/NSKeyValueBinding.h中的非正式协议NSKeyValueBindingCreation来声明。