KVC简介 待续

KVC

Key-value编码是一种由NSKeyValueCoding非正式协议启用的机制,对象采用该协议来提供对其属性的间接访问。当一个对象是key-value编码兼容的,它的属性可以通过一个简洁、统一的消息传递接口通过字符串参数寻址。这种间接访问机制补充了实例变量及其相关访问器方法所提供的直接访问。

通常使用访问器方法来获得对对象属性的访问权。get访问器(或getter)返回属性的值。设置访问器(或setter)设置属性的值。在Objective-C中,你也可以直接访问属性的底层实例变量。以上述任何一种方式访问对象属性都很简单,但需要调用特定于属性的方法或变量名。随着属性列表的增长或更改,访问这些属性的代码也必须增加或更改。相反,符合键值编码的对象提供了一个简单的消息传递接口,该接口在其所有属性之间是一致的。

使用键值编码兼容对象

当对象从NSObject(直接或间接)继承时,通常采用键值编码,NSObject既采用NSKeyValueCoding协议,也为基本方法提供了默认实现。这样的对象允许其他对象通过一个紧凑的消息传递接口完成以下操作:

  • 访问对象的属性。该协议指定了一些方法,比如通用的getter valueForKey:和通用的setter setValue:forKey:,通过名称或参数化为字符串的键来访问对象属性。这些方法和相关方法的默认实现使用键来定位底层数据并与之交互,如访问对象属性中所述。
  • 操作集合属性。访问方法的默认实现使用对象的集合属性(比如NSArray对象),就像使用其他属性一样。此外,如果对象为属性定义了集合访问器方法,则它允许键-值访问集合的内容。这通常比直接访问更有效,并允许您通过一个标准化的接口来处理定制的集合对象,如访问集合属性中所述。
  • 在集合对象上调用集合运算符。当访问符合键值编码的对象中的集合属性时,您可以将集合操作符插入到键字符串中,如使用集合操作符中所述。集合操作符指示默认的NSKeyValueCoding getter实现对集合采取一个操作,然后返回一个新的、过滤后的集合版本,或者一个表示集合某些特征的单个值。
  • 访问非对象属性。协议的默认实现检测非对象属性,包括标量和结构,并自动将它们包装和解包装为在协议接口上使用的对象,如表示非对象值中所述。此外,该协议声明了一个方法,允许一个兼容的对象提供一个合适的操作,以应对通过key-value编码接口在非对象属性上设置nil值的情况。
  • 按键路径访问属性。当您拥有符合键值编码的对象的层次结构时,您可以使用基于键路径的方法调用向下钻取,使用单个调用在层次结构中获取或设置深度值。
对象采用键值编码

为了使你自己的对象key-value编码兼容,你要确保它们采用NSKeyValueCoding非正式协议并实现相应的方法,例如valueForKey:作为一个通用的getter和setValue:forKey:作为一个通用的setter。幸运的是,如上所述,NSObject采用了这个协议,并为这些方法和其他基本方法提供了默认实现。因此,如果你从NSObject(或它的任何子类)派生对象,很多工作已经为你完成了。

为了让默认方法完成它们的工作,您要确保对象的访问器方法和实例变量遵循特定定义良好的模式。这允许默认实现在响应键-值编码的消息时查找对象的属性。然后,您可以通过提供验证和处理某些特殊情况的方法来扩展和定制键值编码。

用Swift进行键值编码

默认情况下,从NSObject或其子类继承的Swift对象的key-value编码符合它们的属性。而在Objective-C中,属性的访问器和实例变量必须遵循特定的模式,Swift中的标准属性声明自动保证了这一点。另一方面,协议的许多特性要么是不相关的,要么是使用native Swift结构或Objective-C中不存在的技术更好地处理。例如,因为所有的Swift属性都是对象,所以你永远不会使用默认实现对非对象属性的特殊处理。

因此,虽然key-value编码协议方法直接转换为Swift,但本指南主要关注的是Objective-C,在Objective-C中,你需要做更多的工作来确保一致性,key-value编码通常是最有用的。需要在Swift中采用不同方法的情况在指南中都有说明。

其他Cocoa技术依赖于键值编码

key-value编码兼容的对象可以参与依赖于这种访问的各种Cocoa技术,包括:

  • KVO。该机制允许对象注册由另一个对象的属性变化驱动的异步通知,如Key-Value Observing Programming Guide中所述。
  • Cocoa绑定。这组技术完全实现了模型-视图-控制器范式,其中模型封装应用程序数据,视图显示和编辑数据,控制器在两者之间进行协调。阅读Cocoa Bindings Programming Topics了解更多关于Cocoa Bindings的内容。
  • Core Data。该框架为与对象生命周期和对象图管理(包括持久性)相关的常见任务提供了通用的自动化解决方案。你可以在Core Data Programming Guide中阅读Core Data。
  • AppleScript。这种脚本语言可以直接控制可编写脚本的应用程序和macOS的许多部分。Cocoa的脚本支持利用键值编码来获取和设置可脚本对象中的信息。NSScriptKeyValueCoding非正式协议中的方法为处理键值编码提供了额外的功能,包括通过多值键的索引获取和设置键值,以及将键值强制(或转换)为适当的数据类型。AppleScript Overview提供了AppleScript及其相关技术的高级概述。

KVC编程基础

对象通常在其接口声明中指定属性,这些属性属于以下几个类别之一:

  • 属性。这些都是简单的值,比如标量、字符串或布尔值。值对象(如NSNumber)和其他不可变类型(如NSColor)也被认为是属性。
  • 一个关系。这些是具有自身属性的可变对象。对象的属性可以在对象本身不改变的情况下改变。例如,银行帐户对象可能具有一个owner属性,该属性是Person对象的实例,而Person对象本身具有一个地址属性。业主地址可能会改变,但不改变银行帐户持有的业主参考资料。银行账户的所有者没有改变。只有他们的地址有。
  • 许多关系。这些是集合对象。你通常使用NSArray或NSSet的实例来保存这样的集合,尽管也可以自定义集合类。

BankAccount对象的属性

@interface BankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
@end

为了维护封装性,对象通常为其接口上的属性提供访问方法。对象的作者可以显式地编写这些方法,也可以依赖于编译器来自动合成它们。无论哪种方法,使用这些访问器之一的代码作者必须在编译代码之前将属性名写入代码中。访问器方法的名称成为使用它的代码的静态部分。例如,给定清单2-1中声明的银行帐户对象,编译器会合成一个setter,你可以为myAccount实例调用它:

[myAccount setCurrentBalance:@(100.0)];

这是直接的,但缺乏灵活性。另一方面,与键值编码兼容的对象提供了一种更通用的机制,可以使用字符串标识符访问对象的属性。

用键和键路径标识一个对象的属性

键是标识特定属性的字符串。通常,按照约定,表示属性的键是在代码中显示的属性本身的名称。键必须使用ASCII编码,不能包含空格,通常以小写字母开头(不过也有例外,例如在许多类中可以找到URL属性)。

因为清单上面代码中的BankAccount类是符合键值编码的,所以它识别键所有者、currentBalance和事务,这些都是它的属性的名称。与其调用setCurrentBalance:方法,你可以通过键来设置值:

[myAccount setValue:@(100.0) forKey:@"currentBalance"];

实际上,您可以使用相同的方法,使用不同的关键参数来设置myAccount对象的所有属性。因为参数是字符串,所以它可以是在运行时操作的变量。

键路径是一个由点分隔的键组成的字符串,用于指定要遍历的对象属性序列。序列中第一个键的属性相对于接收方,每个后续键的计算都相对于前一个属性的值。关键路径对于使用单个方法调用向下钻取对象的层次结构非常有用。

例如,应用于银行帐户实例的关键路径owner.address.street引用存储在银行帐户所有者地址中的街道字符串的值,假设Person和address类也是符合键值编码的。

在Swift中,您可以使用#keyPath表达式,而不是使用字符串来指示关键字或关键路径。这提供了编译时检查的优势,如使用Cocoa和Objective-C (Swift 3)指南中的Keys和Key Paths部分所述。

使用键获取属性值

当对象采用NSKeyValueCoding协议时,是key-value编码兼容的。一个继承自NSObject的对象,它提供了协议基本方法的默认实现,自动采用该协议的某些默认行为。这样的对象至少实现了以下基本的基于键的getter:

  • valueForKey:—返回由key参数命名的属性的值。如果键命名的属性不能根据访问器搜索模式中描述的规则找到,那么对象会给自己发送一个valueForUndefinedKey:消息。valueForUndefinedKey的默认实现:会引发一个NSUndefinedKeyException,但子类可能会重写此行为,并更优雅地处理这种情况。
  • valueForKeyPath:—返回指定的key路径相对于接收者的值。键路径序列中的任何对象,如果不符合某个特定键的键值编码(即valueForKey:的默认实现无法找到访问器方法),则接收valueForUndefinedKey:消息。
  • dictionaryWithValuesForKeys:—返回相对于接收方的键数组的值。该方法为数组中的每个键调用valueForKey:。返回的NSDictionary包含数组中所有键的值。

集合对象,比如NSArray, NSSet和NSDictionary,不能包含nil作为值。相反,你可以使用NSNull对象来表示nil值。NSNull提供了一个实例来表示对象属性的nil值。dictionaryWithValuesForKeys:和相关的setValuesForKeysWithDictionary:的默认实现会自动在NSNull(在字典参数中)和nil(在存储属性中)之间转换。

当您使用键路径来寻址一个属性时,如果键路径中除了最后一个键之外还有对多关系(也就是说,它引用了一个集合),则返回值是一个包含对多键右侧键的所有值的集合。例如,请求键路径事务的值。收款人返回包含所有交易的收款人对象的数组。这也适用于键路径中的多个数组。关键路径accounts.transactions.payee返回一个包含所有账户中所有交易的所有收款人对象的数组。

使用键设置属性值

与getter一样,key-value编码兼容的对象也提供了一小群通用setter,它们的默认行为基于NSObject中找到的NSKeyValueCoding协议的实现:

  • setValue:forKey:—将指定的key相对于接收消息的对象的值设置为给定的值。forKey的默认实现自动展开表示标量和结构的NSNumber和NSValue对象,并将它们分配给属性。有关包装和展开语义的详细信息,请参见表示非对象值。
    如果指定的key对应的属性是接收setter调用的对象所不具备的,该对象就会给自己发送一个setValue:forUndefinedKey:消息。setValue:forUndefinedKey的默认实现会引发一个NSUndefinedKeyException异常。但是,子类可以重写此方法以自定义方式处理请求。
  • setValue:forKeyPath:—在指定的key路径上设置相对于接收者的给定值。在键路径序列中,任何不符合键值编码的对象都会收到一个setValue:forUndefinedKey:消息。
  • setValuesForKeysWithDictionary:—用指定字典中的值设置接收者的属性,使用字典键来识别属性。默认的实现为每个键值对调用setValue:forKey:,根据需要将NSNull对象替换为nil。

在默认实现中,当你尝试将一个非对象属性设置为nil值时,符合key-value编码的对象会给自己发送一个setNilValueForKey:消息。setNilValueForKey的默认实现:引发一个NSInvalidArgumentException,但是一个对象可以覆盖这个行为来替换一个默认值或一个标记值,如处理非对象值中所述。

使用键简化对象访问

要了解基于键的getter和setter如何简化代码,请考虑以下示例。在macOS中,NSTableView和NSOutlineView对象将标识符字符串与它们的每一列关联起来。如果支持表的模型对象不符合键值编码,表的数据源方法将被迫依次检查每个列标识符,以找到要返回的正确属性。而且,将来在向模型添加另一个属性时(在本例中是Person对象),还必须重新访问数据源方法,添加另一个条件来测试新属性并返回相关值。

// 2-2不需要键值编码的数据源方法的实现
- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    id result = nil;
    Person *person = [self.people objectAtIndex:row];
 
    if ([[column identifier] isEqualToString:@"name"]) {
        result = [person name];
    } else if ([[column identifier] isEqualToString:@"age"]) {
        result = @([person age]);  // Wrap age, a scalar, as an NSNumber
    } else if ([[column identifier] isEqualToString:@"favoriteColor"]) {
        result = [person favoriteColor];
    } // And so on...
 
    return result;
}

另一方面,2-3显示了相同数据源方法的更紧凑的实现,它利用了符合键值编码的Person对象。仅使用valueForKey: getter,数据源方法使用列标识符作为键返回适当的值。除了更短之外,它也更通用,因为当以后添加新列时,只要列标识符始终匹配模型对象的属性名称,它就可以继续工作。

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    return [[self.people objectAtIndex:row] valueForKey:[column identifier]];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值