KVC
简单介绍一下KVC(Key-Value Coding)
起步
关于键值编码(KVC)
KVC是一种NSKeyValueCoding非正式协议支持的机制,对象采用这种机制来提供对其属性的间接访问。当对象符合KVC时,则可通过这种统一、简洁的消息传递接口方式以字符串参数形式访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问方式。
通常情况下,可以通过使用访问器方法来访问对象的属性。getter返回属性的值。setter设置属性的值。在Objective-C中,也可以直接访问属性的带下划线的实例变量。以这些方式访问对象的属性是很直接的方式,但是需要调用特定的属性方法或变量名称。随着属性列表的增加或更改,访问这些属性的代码也需随着增加或更改。相反,符合KVC的对象提供了一个简单的消息传递接口,该接口在其所有属性之间都是一致的。
KVC是许多其他Cocoa技术的基础概念,例如KVO、Cocoa绑定、Core Data,AppleScript-ability。KVC在某些情况下还可以帮助我们简化代码。
使用符合KVC的对象
当对象从NSObject(直接或间接)继承时,通常都实现了KVC,即该对象既采用了NSKeyValueCoding协议,也提供了基本方法的默认实现。这样的对象可以通过紧凑的消息传递接口使其他对象可以执行以下操作:
- 访问对象属性。该协议指定了一些方法,比如通用的getter方法valueForKey:和通用的setter方法setValue:forKey:,通过参数为字符串形式用于名称或键来访问对象属性。这些方法和相关方法的默认实现使用键来与基础数据的定位和交互,如访问对象属性中所述。
- 操作集合属性。与其他任何属性一样的,访问方法的默认实现与对象的集合属性(如NSArray对象)一起使用。另外,如果对象为属性定义了集合访问器方法,则将启用对集合内容的键值访问。这样通常比直接访问更有效,并允许通过标准接口使用自定义集合对象,如访问集合属性中所述。
- 在集合对象上调用集合运算符。当访问符合KVC的对象中的集合属性时,可以将集合运算符插入到键字符串中,如使用集合运算符所述。集合运算符指令默认的NSKeyValueCoding的getter实现对集合进行操作,然后返回一个新的、经过过滤的集合版本或表示集合某些特征的单个值。
- 访问非对象属性。协议的默认显示检测到非对象(包括标量和结构体)属性时,会在协议接口上自动将它们包装和解包为对象,如表示非对象值所述。另外,该协议声明了一个方法允许符合的对象为通过KVC接口在非对象属性上设置nil值的情况提供适当的操作。
- 通过键路径来访问属性。当有一个符合KVC对象的层次结构时,可以使用基于键路径的方法调用去深入、获取、设置一个深层次结构的值仅通过一个方法。
使对象符合KVC
为了使自己的对象符合KVC,请确保其采用了NSKeyValueCoding非正式协议并实现了相应的方法,比如valueForKey:作为通用的getter、setValue:forKey:作为通用的setter。幸运的是,如上所述,NSObject采用了该协议并为这些和其他必要方法提供了默认实现。因此,如果从NSObject或其子类中派生对象,则很多工作已经为您完成了。
为了使默认方法能够正常运行,需要确保对象的访问器方法和实例变量采用了明确定义的模式。这允许默认实现根据KVC的消息查找到对象的属性。然后可以选择通过提供验证方法和处理某些特殊情况的方法来扩展和自定义KVC。
Swift的KVC
从NSObject或其子类继承的Swift对象默认情况下对其属性遵循KVC。在Objective-C中,属性的访问器和实例变量必须遵循一定模式,而Swift中的标准属性声明会自动保证这一点。另一方面,许多协议的功能要么不相关,要么被Swift本身的构造或技术更好的处理而这些不存在于Objective-C中。例如,由于所有的Swift属性都是对象,因此永远不会使用对非对象属性的默认实现的特殊处理。
其他基于KVC的Cocoa技术
符合KVC的对象可以参与各种依赖于这种访问方式的Cocoa技术,包括:
- KVO。这种机制使对象可以注册由另一个对象属性的变更驱动的异步通知,详见键值观察编程指南所述。
- Cocoa绑定。这一系列技术完全实现了“Model-View-Controller”范例,其中Model封装了应用程序的数据,views显示和编辑该数据,controllers作为两者之间的中介。阅读Cocoa绑定编程主题了解更多关于Cocoa绑定的信息。
- Core Data。该框架为对象的生命周期和对象图形化管理(包括持久化)相关的常见任务提供了通用的自动化解决方案。可以阅读Core Data编程指南获取Core Data更多信息。
- AppleScript。这种脚本语言可以直接控制可编写脚本的应用程序和macOS的很多部分。Cocoa的脚本支持利用KVC来获取和设置脚本化对象的信息。NSScriptKeyValueCoding非正式协议中的方法提供了使用KVC的其他功能,包括在多键值中通过索引来获取和设置键值,以及将键值强转或转换为适当的数据类型。AppleScript概览提供了AppleScript和其相关技术的高级概述。
KVC基础
访问对象属性
一个对象通常在其接口声明中指定属性,这些属性属于以下几类之一:
-
属性。这些是简单值,比如标量、字符串、布尔值。值对象如NSNumber和其他不可变类型比如NSColor也被视为属性。
-
一对一的关系。这些是具有自己属性的可变对象。一个对象属性可以变更,而不是对象本身发生变更。比如,银行账户对象有一个所有者属性为Person对象实例,Person对象本身具有address属性。所有者的地址可能更改,而无需更改银行账户持有的所有者引用。银行账户的所有者未发生变更,只有它的地址发生了变化。
-
一对多的关系。这些是集合对象。尽管也可以使用自定义的集合类,但通常使用NSArray或NSSet实例来保存此类集合。
为了维护封装,对象通常为其接口上属性提供访问器方法。可以显示地编写这些方法或依赖编译器自动合成它们。无论哪种方式,使用这些访问器都需要在编译之前在代码中写明属性名。访问器方法的名称成为使用它的代码的静态部分。
这样的方式直接但缺乏灵活性。另外,符合KVC的对象提供了一种更通用的机制,可以使用字符串标识访问对象的属性。
通过键和键路径识别对象的属性
键是标识特定属性的字符串。通常,按照约定,代表属性的键是该属性本身在代码中出现的名称。键必须使用ASCII编码,不能包含空格,并通常以小写字母开头(尽管有例外,例如在许多类中可以看到URL属性)。
因为参数是字符串,所以它可以是在运行时进行操作的变量。
键路径是一串点分隔的键,用于指定要遍历的对象属性序列。序列中第一个键的属性是相对于接收者的,并且每个后续键都是相对于前一个属性的值进行计算的。键路径对于通过单个方法调用向下获取到对象的层次结构是很有用的。
注意: 在Swift中,可以使用#keyPath表达式来代替使用字符串来表示键或键路径。这提供了编译时检查的优势。
使用键获取属性值
当对象采用NSKeyValueCoding协议时即符合KVC。继承自NSObject的对象(提供了该协议基本方法的默认实现)会自动遵守协议并具有某些默认行为。这样的对象至少实现以下基本的基于键的getter方法:
- valueForKey: 返回由key参数命名的属性的值。如果根据访问者搜索模式中描述的规则找不到由键命名的属性,则该对象会向自身发送valueForUndefinedKey:消息。valueForUndefinedKey:方法的默认实现会引出一个NSUndefinedKeyException异常,但是子类可以通过重写此行为以便更优雅的处理该情况。
- valueForKeyPath:返回相对于接收者的指定键路径的值。键路径序列中不符合KVC的指定键的任何对象(即valueForKey:的默认实现无法找到访问器方法)都接收到一个valueForUndefinedKey:消息。
- dictionaryWithValuesForKeys:返回相对于接收者的键数组的值。该方法为数组中的每个键调用valueForKey:方法。返回的NSDictionary包含数组中所有键的值。
注意: 集合对象(如NSArray、NSSet、NSDictionay)不能包含nil作为值。但是可以使用NSNull对象来表示nil值。NSNull提供了一个单独的实例来表示对象属性的nil值。dictionaryWithValuesForKeys:方法和相关的setValuesForKeysWithDictionary:方法的默认实现自动在NSNull(在字典参数重)和nil(在存储的属性中)之间转换。
当使用键路径寻址属性时,如果除了键路径中的最后一个键以外的键都是一对多关系(即它引用一个集合),则返回值是一个右边是对多的键的包含键的所有值的集合。比如,请求键路径transactions.payee的值返回一个数组包含了所有payee对象这也适用于键路径中有多个数组的情况。键路径accounts.transactions.payee返回的数组包含所有accounts中所有transactions的所有payee对象。
使用键设置属性
与getter一样,符合KVC的对象还根据NSObject中的NSKeyValueCoding协议的实现提供了一小组默认行为的广义setter:
- setValue:forKey: 将相对于接收消息的对象的指定键的值设置为给定值。setValue:forKey:的默认实现可以自动解包NSNumber和NSValue对象来表示标量和struct,并将它们分配给属性。有关包装和解包语义的细节,请参见表示非对象值。如果指定的键对应的接收setter调用的对象没有这个属性,则该对象将向自身发送setValue:forUndefinedKey:消息。setValue:forUndefinedKey:方法的默认实现是引发一个NSUndefinedKeyException异常。但是,子类可以重写此方法以自定义方式处理请求。
- setValue:forKeyPath: 在相对接收者的指定键路径处设置给定值。键路径序列中不符合特定键的KVC的任何对象都会收到setValue:forUndefinedKey:消息。
- setValuesForKeysWithDictionary: 使用字典键标识属性,使用指定字典中的值设置接收者的属性。默认实现为每个键值对调用setValue:forKey: 方法,并根据需要用nil替换NSNull。
在默认实现中,当尝试将非对象属性设置为nil时,符合KVC的对象会向自身发送setNilValueForKey:消息。setNilValueForKey:默认的实现是引发NSInvalidArgumentException异常,但是对象可以重写此行为以替换默认值或标记值,如处理非对象值中所述。
使用键来简化对象访问
访问集合属性
符合KVC的对象以公开其他属性相同的方式公开其对多的属性。可以像使用valueForKey:和setValue: forKey: (或它们的等效键路径)像其他对象一样获取或设置集合对象。但是,当要操纵这些集合的内容时,通常使用协议定义的可变代理方法更高效。
该协议为访问集合对象定义了三种不同的代理方法,每种方法都有一个键和一个键路径变量:
- mutableArrayValueForKey: 和 mutableArrayValueForKeyPath: 它们返回一个代理对象,其行为类似于NSMutableArray对象。
- mutableSetValueForKey: 和 mutableSetValueForKeyPath: 它们返回一个代理对象,其行为类似于NSMutableSet对象。
- mutableOrderedSetValueForKey: 和 mutableOrderedSetValueForKeyPath: 它们返回一个代理对象,其行为类似于NSMutableOrderedSet对象。
当对代理对象进行操作,向其中添加、删除或替换对象时,协议的默认实现都会相应地修改基础属性。这比使用valueForKey:获取一个不可变的集合对象,创建一个内容以修改的修改对象,然后使用setValue: forKey:消息将其存回该对象更有效。大多数情况下,这比直接使用可变属性更高效。这些方法提供了额外的好处是可以对集合对象中保存的对象保持其遵守KVO。(详情请参见KVO编程指南)
使用集合运算符
当向符合KVC的对象发送valueForKeyPath:消息时,可以将集合运算法嵌入到键路径中。集合运算符是其后带有@符号的一小部分关键字之一,该符号指定getter在返回数据之前应执行的操作以某种方式操作数据。有NSObject提供的valueForKeyPath:的默认实现会实现此行为。
当键路径包含集合运算符时,该运算符之前的键路径的任何部分(称为左键路径)标明相对于消息接收者进行操作的集合。如果将消息直接发送给集合对象(如NSArray实例),则可以省略左键路径。
运算符之后的键路径部分(称为右键路径)指定了运算符应该在集合中进行操作的属性。@count除外的所有集合运算符都需要正确的键路径。
图4-1 操作键路径格式
集合运算符表现为三种基本的行为类型:
- 聚合操作符以某种方式合并集合的对象,并返回与右键路径中属性名的数据类型相匹配的单个对象。@count运算符是个例外,它不使用右键路径并始终返回NSNumber实例。
- 数组运算符返回一个NSArray实例,包含命名集合中持有的对象的某些子集。
- 嵌套运算符在包含其他集合的集合上工作,并返回NSArray或NSSet实例(取决于运算符),该实例以某种方式组合了嵌套集合的对象。
集合运算符
聚合运算符处理数组或一组属性,产生一个反映集合某些方面的值。
@avg
当指定@avg运算符时,valueForKeyPath:会为集合的每个元素读取由右键路径指定的属性,将其转换为double(0替换为nil值),然后计算这些值的算术平均值。然后返回存储在NSNumber实例中的结果。
@count
当指定@count运算符时,valueForKeyPath:返回NSNumber实例中的集合对象数。如果存在右路径会被忽略。
@max / @min
当指定@max / @min运算符时,valueForKeyPath:在由右键路径命名的集合条目中搜索并返回最大/最小的一个。搜索使用compare:方法进行比较,该方法由许多Foundation类定义(如NSNumber类)。因此,右键路径表示的属性必须包含一个有意义地响应此消息的对象。搜索将忽略nil值的集合条目。
@sum
当指定@sum运算符时,valueForKeyPath:会为集合的每个元素读取由右键路径指定的属性,将其转换为双精度(将0替换为nil值),然后计算它们的总和。然后返回存储在NSNumber实例中的结果。
数组运算符
数组运算符使valueForKeyPath:返回一个对象数组,其中对象与右键路径表示的一组特定对象对应。
重要 使用数组运算符时,如果任何叶子对象为nil,则valueForKeyPath:方法将引发异常。
@distinctUnionOfObjects
当指定@distinctUnionOfObjects运算符时,valueForKeyPath:创建并返回一个数组,该数组包含与右键路径指定当属性相对应当集合的不同对象。
注意 @unionOfObjects运算符提供类似的行为,但不删除重复的对象。
@unionOfObjects
返回的数组包含对应的所有对象。与@distinctUnionOfObjects不同,不会删除重复的对象。
嵌套运算符
嵌套运算符对嵌套集合进行操作,其中集合的每个条目都包含一个集合。
重要 使用嵌套运算符时,如果任何叶子对象为nil,则valueForKeyPath:方法将引发异常。
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets
表示非对象值
NSObject提供的KVC协议方法的默认实现可以同时用于对象和非对象的属性。默认实现自动在对象参数或返回值与非对象属性之间转换。这允许基于键的getter和setter方法的签名保持一致,即便存储的属性是一个标量或一个结构体。
注意 由于Swift中的所有属性都是对象,因此本节仅介绍Objective-C属性。
当调用协议的getter时,比如valueForKey:,默认实现将根据访问者搜索模式中描述的规则来确定指定键提供值的特定访问器方法或实例变量。如果返回值不是对象,则getter方法使用此值来初始化NSNumber(用于标量)或NSValue(用于结构体)对象并返回该对象。
类似地,默认情况下,setter方法使用setValue:forKey:之类的方法时,确定数据的类型需要属性的访问器或实例变量给定特定的键。如果数据类型不是对象,则setter首先适当的<type>Value消息发送到传入值对象以提取基础数据,然后再存储提取后的数据。
注意 当为非对象属性调用的KVC协议setters方法值为nil时,setter没有明显的、一般的步骤去执行。因此,将发送setNilValueForKey:消息给接收setter调用的对象。此方法默认实现会引发NSInvalidArgumentException异常,但是子类可以重写此行为,详情见处理非对象值,比如设置标记值或提供有意义的默认值。
包装和解包标量类型
注意 因历史原因,macOS中BOOL类型定义为带符号char,KVC区分不了这些。因此,当键是BOOL时,不应该传如@”true”或@”YES”之类当字符串值给setValue:forKey:。KVC将尝试调用charValue(因为BOOL本质上是char类型),但NSString没有实现此方法,会导致一个运行时错误。所以,当键是BOOL时,仅仅可以传NSNumber对象,如@(1)或@(YES),作为 setValue:forKey:的value参数。此限制不适用于iOS中,iOS中BOOL的类型定义为原生Boolean类型且KVC调用boolValue,故适用于NSNumber对象或格式正确的NSString对象。
包装和解包结构体类型
常见结构体有NSPoint, NSRange, NSRect, and NSSize。但不限于此,可以将结构体类型(即,其Objective-C类型编码字符串以{开头的类型)包装在NSValue对象中。
验证属性
KVC协议定义了支持属性验证的方法。正如使用基于键的访问器读写符合KVC对象的属性一样,也可以按照键(或键路径)验证属性。当调用validateValue:forKey:error: 或 validateValue:forKeyPath:error:方法时,协议的默认实现会在接收到验证消息(或键路径末尾部分)的对象中搜索与验证模式<Key>:error:匹配的方法名。如果此对象没有这个方法,则默认验证成功,且默认实现返回YES。当存在特定属性验证方法时,默认实现将返回调用该方法的结果。
注意 此处验证的描述仅用于Objective-C中。
由于特定属性的验证方法通过引用接收值和错误参数,因此验证有三种可能结果:
- 验证方法认为值对象有效并在不更改值或错误的情况下返回YES。
- 验证方法认为值对象无效,但选择不对其进行修改。这种情况下,该方法返回NO并将错误引用(如果调用者提供了的话)设置为指示错误原因的NSError对象。
- 验证方法认为值对象无效,但是创建并替换为一个新的有效对象。这种情况下,该方法返回YES并保持错误对象不变。在返回之前,该方法修改值引用为指向为新的值对象。进行修改时,该方法始终创建一个新对象,而不是修改旧对象,即使这个值对象是可变的。
自动验证
通常,KVC协议和其默认实现都未定义任何机制来自动执行验证。而是在应用程序适当的时候自己使用验证方法。
其他的Cocoa技术在某些情况下会自动执行验证。如,Core Data会在保存管理对象上下文时自动执行验证(参见Core Data指南)。另外,在macOS中,Cocoa绑定允许指定验证在发生时自动进行(更多信息见Cocoa绑定编程主题)。
访问器搜索模式
NSObject提供的NSKeyValueCoding协议的默认实现使用一组明确定义的规则将基于键的访问器映射到对象的基础属性。这些协议方法使用键参数在自己的对象实例中搜索遵循某些命名约定的访问器、实例变量、相关方法。很少修改默认搜索,这有助于了解它的工作方式,且有利于跟踪键值编码对象的行为,及使自定义对象符合KVC。
注意: 本节描述的使用<key>或<Key>作为键字符串的占位符,该键字符串在KVC协议方法中作为参数出现,然后由该方法作为辅助方法调用的一部分或变量名寻找。映射的属性名称遵守占位符的大小写。比如,对于getter <key> 和 is <key>,属性名为hidden映射为hidden和isHidden。
基本Getter的搜索模式
valueForKey:的默认实现,在给定键参数作为输入时,执行以下过程,从接收valueForKey:调用的类实例内部进行操作。
- 依次在名称类似于get<Key>、<key>、is<Key>、_<key>搜索实例中找到的第一个访问器方法。如果找到,则调用它并和结果传入执行步骤5。否则,请继续下一步。
- 如果未找到简单的访问器方法,在实例中搜索名称与模式 countOf<Key> 、 objectIn<Key>AtIndex:(对应于NSArray类定义的原始方法)、
AtIndexes:(对应于NSArray方法objectAtIndexes:)。如果找到第一个和其他两个中至少一个,则创建并返回一个响应所有NSArray方法的集合代理对象。否则,将继续执行步骤3。代理对象随后将接收到的所有NSArray消息转换为countOf\<Key\>、objectIn\<Key\>AtIndex、\<Key\>AtIndexes:消息的某种组合以创建键值编码对象。如果原始对象还实现了名为get\<Key\>:range:之类的可选方法,则代理方法也会在适当的时候使用该方法。实际上,代理对象与符合KVC的对象一起使用,可以使基础属性的行为类似于NSArray。 - 如果未找到简单的访问器方法或数组访问方法组,则查找名为countOf
, enumeratorOf , memberOf :的三重方法(对应与NSSet类定义的原始方法)。如果找到所有的三个方法,则创建并返回一个响应所有的NSSet方法的集合代理对象。否则就执行步骤4。此代理对象随后将其接收到的所有的NSSet消息转换为countOf , enumeratorOf , memberOf :消息的某种组合以创建对象。实际上,代理对象与符合KVC的对象一起使用,是基础属性的行为表现为NSSet一样,就是它不是。 - 如果未找到简单的访问器方法或一组集合访问方法,并接收者的类方法accessInstanceVariablesDirectly返回YES,则按照名称为 _
, _is , , is 的顺序搜索对象变量。如果找到,则直接获取实例变量的值,然后执行步骤5。否则,执行步骤6。 - 如果检索到的属性值是一个对象指针,则简单的返回结果。如果该值是NSNumber支持标量属性,则将其存储到NSNumber实例中并返回它。如果结果是NSNumber不支持的标量类型,则转换为NSValue对象并返回它。
- 如果所有其他方法都失败了,则调用valueForUndefinedKey:。默认情况下,会引发一个异常,NSObject的子类可以提供特定键的行为。