开始
关于键值编码
键值编码是一种机制,通过NSKeyValueCoding
非正式协议,对象采用这种机制提供对其属性的间接访问。当对象符合键值编码时, 它的属性可以使用字符串参数通过简明、统一的消息接口进行寻址。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。
通常使用访问器方法获取对对象属性的访问权限。get
访问器 (或 getter
) 返回属性的值。set
访问器 (或 setter
) 设置属性的值。在Objective-C
中, 还可以直接访问属性内部的实例变量。以上述任何一种方式访问对象属性都非常直截了当, 但需要调用属性特定的方法或变量名。随着属性列表的增长或更改, 必须编写访问这些属性的代码。相反, 键值编码兼容对象提供了一个在其所有属性中一致的简单消息传递接口。
键值编码是许多其他Cocoa
技术的基础概念, 如KVO
、Cocoa bindings
、Core Data
和 AppleScript-ability
。在某些情况下, 键值编码还可以帮助简化代码。
使用键值编码兼容对象
对象通常采用键值编码, 当它们 (直接或间接) 继承NSObject
时, 它们都采用了NSKeyValueCodin
协议, 并为基本方法提供默认实现。此类对象使其他对象能够通过简洁的消息接口执行以下操作:
访问对象属性. 协议中指定的方法, 例如一般的 getter
valueForKey:
和常用的settersetValue:forKey:
, 用于通过名称或键访问对象属性 (参数化为字符串)。这些方法和相关方法的默认实现使用key来定位基础数据并与之交互, 详见访问对象属性.操作集合属性. 配合对象集合属性(如
NSArray
对象)使用的访问器方法的默认实现与对象其他属性的访问器方法一样。此外, 如果对象定义了属性的集合访问器方法, 则它允许对集合的内容进行键值访问。这通常比直接访问效率更高, 并允许你通过标准化的接口处理自定义集合对象, 详见访问集合属性.在集合对象上调用集合运算符. 在键值编码兼容对象中访问集合属性时, 可以将集合运算符插入到键字符串中, 详见使用集合运算符。集合运算符指示
NSKeyValueCoding
getter 默认的实现去对集合执行某些操作, 然后返回一个全新的、过滤过的集合版本或单个表示某些集合特性的值 。访问非对象属性. 协议的默认实现支持检测非对象属性, 包括标量和结构, 并自动将它们包装成对象和解包,详见表示非对象值.此外, 该协议还声明了一种方法, 允许兼容对象通过键值编码接口在非对象属性上设置
nil
值时为该情况提供适当的操作。按键路径访问属性. 当你有一个键值编码兼容的对象的层级结构,你可以使用
key path based
方法在单次调用中获取或设置层次结构深处的值。
为对象采用键值编码
为了使您自己的对象兼容键值编码, 您可以确保它们遵循NSKeyValueCoding
非正式协议并实现相应的方法, 如valueForKey:
作为一般的 getter 和setValue:forKey:
作为一般的 setter。幸运的是, 如上文所述, NSObject
遵循此协议, 并为这些和其他基本方法提供了默认实现。因此, 如果从NSObject
(或它的许多子类) 中派生对象, 则大部分工作已经完成。
为了使默认方法执行其工作, 您可以确保对象的访问器方法和实例变量遵守某些定义良好的模式。这允许默认实现在响应键值编码消息时查找对象的属性。然后, 您可以通过提供验证方法和处理某些特殊情况来扩展和自定义键值编码。
Swift中的键值编码
默认情况下, 继承自NSObject
或其子类的 Swift
对象,其属性是支持键值编码的。而在Objective-C 中, 属性的访问器和实例变量必须遵循某些模式, Swift 中的标准属性声明会自动保证这一点。另一方面, 许多协议的功能要么不相关, 要么使用某些Objective-C中没有的Swift 原生构造和技术能够更好地处理。例如, 由于所有 Swift 属性都是对象, 因此你永远不会使用到默认实现中对非对象属性的特殊处理。
虽然键值编码协议方法直截了当地翻译成Swift, 但本指南主要侧重于Objective-C, 在这里您需要做更多的事来确保兼容, 而键值编码通常最有用。在整个指南中都需要注意到在 Swift 中采取明显不同方法的情况。
有关使用 swift 与Cocoa技术的详细信息, 请参阅Using Swift with Cocoa and Objective-C (Swift 4.1)。有关 swift 的完整说明, 请阅读The Swift Programming Language (Swift 4.1).
其他依赖于键值编码的Cocoa技术
键值编码兼容的对象可以广泛的应用于依赖这种访问的Cocoa技术, 其中包括:
键值观察. 此机制使对象可以注册由其他对象属性中的更改驱动的异步通知, 详见Key-Value Observing Programming Guide.
Cocoa bindings. 此技术集完全实现了模型-视图-控制器范式, 其中模型封装应用程序数据、视图显示和编辑数据以及控制器在两者之间进行协调。阅读Cocoa Bindings Programming Topics了解有关Cocoa Bindings的更多信息。
Core Data. 此框架为与对象生命周期和对象图管理 (包括持久性) 相关的常见任务提供了通用和自动解决方案。您可以在Core Data Programming Guide中阅读有关Core Data的信息。.
AppleScript 这种脚本语言可以直接控制可脚本化的应用程序和 macOS 的许多部分。Cocoa的脚本支持利用键值编码来获取和设置可脚本化对象中的信息。
NSScriptKeyValueCoding
非正式协议中的方法为使用键值编码提供了额外的功能, 包括按多值键中的索引获取和设置键值, 并将键值强制 (或转换) 为适当的数据类型。AppleScript Overview提供了 AppleScript 及其相关技术的高级别概述。
键值编码基本原理
访问对象属性
对象通常在其接口声明中指定属性, 这些属性属于以下几个类别之一:
-
属性. 一些简单的值, 例如标量(
scalars
)、字符串或布尔值。值对象(如NSNumber
)和其他不可变类型(如NSColor
) 也被视为属性。 - 对一关系. 指具有自身属性的可变对象。对象的属性可以在对象本身没有更改的情况下进行更改。
-
对多关系. 指集合对象。你通常使用
NSArray
或NSSet
的实例来保存此类集合, 自定义集合类也是可行的。
清单 2-1中声明的BankAccount
对象演示了每种类型的属性。
清单 2-1BankAccount
对象的属性
@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中声明的银行帐户对象, 编译器将合成一个可为myAccount实例调用的 setter:
[myAccount setCurrentBalance:@(100.0)];
这很直接, 但缺乏灵活性。另一方面, 一个键值编码兼容对象提供了一个更通用的机制来使用字符串标识符访问对象的属性。
使用键和键路径标识对象的属性
键key
是标识特定属性的字符串。通常, 按照约定, 表示属性的键key
是属性本身在代码中显示的名称。键key
必须使用 ASCII
编码, 不能包含空格, 通常以小写字母开头 (尽管有例外, 如在许多类中找到的URL
属性)。
因为清单 2-1中的BankAccount
类是符合键值编码的, 所以它能识别键(即其属性的名称)owner
、currentBalance
和transactions
。您可以通过其键来设置值, 而不是调用setCurrentBalance:
方法:
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
实际上, 可以使用不同的键参数通过相同方法来设置myAccount对象的所有属性。因为参数是字符串类型, 所以它可以在运行时操作变量。
键路径Key path
是一个用点操作符.
来分隔键的字符串, 用于指定要遍历的对象属性序列。序列中第一个键的属性相对于接收者, 每个后续键相对于上一个属性的值进行计算。键路径对于使用单个方法深入调用对象的层次结构很有用。
例如, 应用于银行帐户实例的键路径owner.address.street
是指存储在银行帐户所有者地址中的街道字符串的值, 假设Person
和Address
类也符合的键值编码。
注意
在 Swift 中, 您可以使用#keyPath
表达式, 而不是使用字符串来指示键或键路径。这提供了编译期间检查的优点, 详见Using Swift with Cocoa and Objective-C (Swift 4.1) 中的Keys and Key Paths章节。
使用键获取属性值
当对象遵循 NSKeyValueCoding
非正式协议,则该对象支持键值编码。从NSObject
(提供了协议中基本方法的默认实现)继承的对象, 它会自动采用此协议的某些默认行为。这样的对象至少实现以下基本的key-based
的 getter:
valueForKey:
返回由键参数命名的属性的值。如果根据访问器搜索模式中描述的规则无法找到由键命名的属性, 则该对象将自己发送valueForUndefinedKey:
消息。valueForUndefinedKey
的默认实现将抛出NSUndefinedKeyException
异常, 但子类可以重写此行为并更优雅地处理此情况。valueForKeyPath:
返回相对于接收者的指定键路径的值。键路径序列中指定键所对应的对象如果不是键值编码兼容的, 即valueForKey:
的默认实现无法找到访问器方法-那么就会接收valueForUndefinedKey:
消息。dictionaryWithValuesForKeys:
返回相对于接收者的一组键所对应的值。该方法为数组中的每个键调用valueForKey:
。返回的NSDictionary
包含数组中所有键的值。
注意
集合对象 (如NSArray
、NSSet
和NSDictionary
) 不能包含nil作为值。而是使用NSNull对象表示nil值。NSNull提供一个表示对象属性的nil值的单个实例。dictionaryWithValuesForKeys:的默认实现和相关的setValuesForKeysWithDictionary:会在NSNull (在字典参数中) 和nil(在存储的属性中)之间进行自动转换 。
当您使用键路径来寻址属性时, 如果键路径中的最后一个键是一对多关系 (即引用集合), 则返回的值是一个集合, 其中包含对多关系的键右侧的键的所有值。例如, 请求键路径 "transactions.payee
" 的值返回包含所有transaction
对象中payee
对象的数组。这也适用于键路径中的多个数组。键路径accounts.transactions.payee
返回包含所有帐户中所有交易记录的所有收款人对象的数组。
使用键设置属性值
与 getter
一样, 键值编码兼容对象还提供了一小组具有默认行为的广义 setter
, 它基于在NSObject
中对NSKeyValueCoding
协议的实现:
-
setValue:forKey:
设置相对于接收到给定值消息的对象的指定键的值。setValue:forKey:
的默认实现会自动对表示标量和结构的NSNumber
和NSValue
对象执行unwarp
操作,并将它们设置到相应的属性中。有关warp
和unwarp
的详细信息, 详见表示非对象值。
如果接收setter
调用的对象中没有对应指定键的属性,该对象将自己发送一个[setValue:forUndefinedKey:]
消息。setValue:forUndefinedKey:
的默认实现将抛出NSUndefinedKeyException
异常。但是, 子类可以重写此方法以自定义方式处理请求。
setValue:forKeyPath:
在相对于接收者的指定键路径上设置给定值。键路径序列中指定键所对应的对象如果不是键值编码兼容的,将会收到setValue:forUndefinedKey:
消息。setValuesForKeysWithDictionary:
将指定字典中的值设置到接收者的属性中, 使用字典键标识属性。默认实现调用每个键值对的setValue:forKey:
, 根据需要用nil
替换NSNull
对象。
在默认实现中, 当您尝试将非对象属性设置为nil
值时, 键值编码兼容对象将自己发送一个setNilValueForKey:
消息。setNilValueForKey:
的默认实现将抛出[NSInvalidArgumentException]
异常, 但对象可能会重写此行为以替换默认值或标记值, 详见处理非对象值。
使用键简化对象访问
想知道基于键的 getter
和 setter
如何简化代码, 请查看下面的示例。在 macOS 中, NSTableView
和NSOutlineView
对象将标识符字符串与每列关联起来。如果表的模型对象不是符合键值编码的, 则表的数据源方法将强制检查每个列标识符, 依次查找要返回的正确属性, 如清单 2-2所示。此外, 在将来, 当您向模型中添加另一个属性时, 在本例中为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, 数据源方法将使用列标识符作为键返回适当的值。除了更短的时间外, 它还更通用, 因为在以后添加新列时, 只要列标识符始终与模型对象的属性名称匹配, 它就会继续保持不变。
清单 2-3基于键值编码的数据源方法的实现
- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row {
return [[self.people objectAtIndex:row] valueForKey:[column identifier]];
}
访问集合属性
键值编码兼容对象以与公开其他属性相同的方式公开其对多属性。您可以像使用valueForKey:
和setValue:forKey:
(或它们的键路径等同方法) 一样获取或设置集合对象。但是, 当您要操作这些集合的内容时, 使用协议定义的可变代理方法通常是最有效的。
该协议为集合对象访问定义了三种不同的代理方法, 每个都具有一个键和一个键路径变体方法:
-
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
这些方法返回的代理对象的行为类似于
NSMutableArray
对象。 -
mutableSetValueForKey:
和mutableSetValueForKeyPath:
这些方法返回的代理对象的行为类似于
NSMutableSet
对象。 -
mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
这些方法返回的代理对象的行为类似于
NSMutableOrderedSet
对象。
当您对代理对象进行操作、向其添加对象、从其中移除对象或替换其中的对象时, 协议的默认实现将相应地修改基础属性。这比使用valueForKey:
得到一个不可变集合对象,创建一个修改了内容的可变集合对象, 然后将其存储回带有setValue:forKey:
消息的对象更有效。在许多情况下, 它也比直接使用可变属性更有效。这些方法提供了对集合对象中保存的对象保持键值观察遵从性的额外好处 (请参见Key-Value Observing Programming Guide以了解详细信息)。
使用集合运算符
当你发送向键值编码兼容的对象发送valueForKeyPath:
消息时, 可以在键路径中嵌入集合运算符。集合运算符是前面有 at 符号 (@
) 的一小部分关键字列表, 它指定了getter
在返回之前以某种方式操作数据。NSObject
提供的valueForKeyPath:
的默认实现实现了此行为。
当键路径包含集合运算符时, 运算符前面的键路径的任何部分 (称为左键路径) 指示相对于消息接收者需要去操作的集合。如果将消息直接发送到集合对象 (如NSArray
实例), 则可以省略左键路径。
图 4-1运算符键路径格式
集合运算符展示了三种基本行为类型:
聚合运算符以某种方式合并集合的对象, 并返回一个通常与在右键路径中命名的属性的数据类型相匹配的单个对象。
@count
运算符是一个特例,它没有右键路径, 并且始终返回NSNumber
实例。数组运算符返回一个
NSArray
实例, 其中包含命名集合中保存的对象的某些子集。嵌套运算符处理包含其他集合的集合, 并返回一个
NSArray
或NSSet
实例 (根据运算符), 它将嵌套集合的对象以某种方式组合在一起。
示例数据
下面的说明包括演示如何调用每个运算符的代码段以及这样做的结果。这依赖于BankAccount
类 (在[列表 2-1]中显示), 它包含Transaction
对象的数组。其中每一个都代表一个简单的checkbook
条目, 如清单 4-1中所声明的那样。
清单 4-1Transaction对象的接口声明
@interface Transaction : NSObject
@property (nonatomic) NSString* payee; // To whom
@property (nonatomic) NSNumber* amount; // How much
@property (nonatomic) NSDate* date; // When
@end
为了进行讨论, 假定BankAccount实例具有一个事务数组, 其中填充了表 4-1中显示的数据, 并使示例从BankAccount对象内部调用。
表 4-1Transactions对象的示例数据
payee | amount | date |
---|---|---|
Green Power | $120.00 | Dec 1, 2015 |
Green Power | $150.00 | Jan 1, 2016 |
Green Power | $170.00 | Feb 1, 2016 |
Car Loan | $250.00 | Jan 15, 2016 |
Car Loan | $250.00 | Feb 15, 2016 |
Car Loan | $250.00 | Mar 15, 2016 |
General Cable | $120.00 | Dec 1, 2015 |
General Cable | $155.00 | Jan 1, 2016 |
General Cable | $120.00 | Feb 1, 2016 |
Mortgage | $1,250.00 | Jan 15, 2016 |
Mortgage | $1,250.00 | Feb 15, 2016 |
Mortgage | $1,250.00 | Mar 15, 2016 |
Animal Hospital | $600.00 | Jul 15, 2016 |
聚合运算符
聚合运算符处理array
或set
属性, 生成一个反映集合的某些方面的单个值。
@avg
当指定@avg运算符时, valueForKeyPath:
读取集合中每个元素的右键路径指定的属性, 将其转换为double (nil值用0替代), 并计算算术平均值。然后返回存储在NSNumber实例中的结果。
获取表 4-1中示例数据之间的平均交易记录金额:
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
transactionAverage
的格式化的结果为 $ 456.54。
@count
指定@count
运算符时, valueForKeyPath:返回一个包含集合中的对象个数的NSNumber实例。右键路径 (如果存在) 将被忽略。
在transactions中获取Transaction对象的数目:
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
numberOfTransactions
的值为13。
@max
指定@max
运算符时, valueForKeyPath:
在由右键路径命名的集合项之间进行搜索, 并返回最大值。搜索使用compare:
方法进行比较, 许多基础类 (如NSNumber
类) 中都有定义。因此, 由右键路径指示的属性必须持有对此消息有意义响应的对象。搜索忽略值为nil
的集合项。
在表 4-1中列出的交易记录中, 获取日期值 (即最新交易记录的日期) 的最大数量:
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
latestDate
的值为 Jul 15, 2016.
@min
指定@min
运算符时, valueForKeyPath:
在由右键路径命名的集合项之间进行搜索, 并返回最小值。搜索使用compare:
方法进行比较, 许多基础类 (如NSNumber
类) 中都有定义。因此, 由右键路径指示的属性必须持有对此消息有意义响应的对象。搜索忽略值为nil
的集合项。
在表 4-1中列出的事务中, 获取日期值 (即最早的事务的日期) 的最短时间。
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
earliestDate
的值为 Dec 1, 2015.
@sum
指定@sum运算符时, valueForKeyPath:
读取集合中每个元素的右键路径指定的属性, 将其转换为double
(nil
值替换为 0), 并计算总和。然后返回存储在NSNumber实例中的结果。
获取表 4-1中示例数据之间的交易记录金额的总和:
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
amountSum
的结果为 $ 5935.00。
数组运算符
数组运算符使valueForKeyPath:
返回与右键路径指示的特定对象集相对应的对象数组。
重要
在使用数组运算符时, 如果有任何叶(leaf
)对象为nil, 则valueForKeyPath:
方法将引发异常。
@distinctUnionOfObjects
指定@distinctUnionOfObjects
运算符时, valueForKeyPath:
将创建并返回一个数组, 其中包含与右键路径指定的属性对应的集合的不同对象。
获取transactions中的交易记录的payee属性值的集合, 但省略了重复值:
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
生成的distinctPayees
数组包含以下每一个字符串实例:Car Loan, General Cable, Animal Hospital, Green Power, Mortgage
。
注意
@unionOfObjects
运算符提供类似的行为, 但不删除重复的对象。
@unionOfObjects
指定@unionOfObjects
运算符时, valueForKeyPath:
将创建并返回一个数组, 其中包含与由右键路径指定的属性对应的集合的所有对象。与@distinctUnionOfObjects
不同, 不删除重复对象。
获取transactions
中的交易记录的payee属性值的集合:
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
生成的payees
数组包含以下字符串:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital
.记录了重复值。
注意
@distinctUnionOfArrays
运算符类似, 但移除重复对象。
嵌套运算符
嵌套运算符对嵌套集合进行操作, 集合本身的每个条目都包含一个集合。
重要
如果在使用嵌套运算符时, 有任何叶(leaf
)对象为nil, 则valueForKeyPath:
方法将引发异常。
对于下面的说明, 请看第二个称为moreTransactions
的数据数组, 其中填充了表 4-2中的数据, 并与原来的transactions
数组一起插入嵌套数组:
NSArray* moreTransactions = @[<# transaction data #>];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
表 4-2 moreTransactions
数组中假设的Transaction
数据
payee | amount | date |
---|---|---|
General Cable - Cottage | $120.00 | Dec 18, 2015 |
General Cable - Cottage | $155.00 | Jan 9, 2016 |
General Cable - Cottage | $120.00 | Dec 1, 2016 |
Second Mortgage | $1,250.00 | Nov 15, 2016 |
Second Mortgage | $1,250.00 | Sep 20, 2016 |
Second Mortgage | $1,250.00 | Feb 12, 2016 |
Hobby Sho | $600.00 | Jun 14, 2016 |
@distinctUnionOfArrays
指定@distinctUnionOfArrays
运算符时, valueForKeyPath:
创建并返回一个数组, 其中包含与右键路径指定的属性相对应的所有集合的组合的不同对象。.
在arrayOfArrays
中的所有数组中获取payee
属性的不同值:
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
生成的collectedDistinctPayees
数组包含以下值: Hobby Shop, Mortgage, Animal Hospital, Second Mortgage, Car Loan, General Cable - Cottage, General Cable, Green Power
。
注意
@unionOfArrays运算符类似, 但不移除重复对象。
指定@unionOfArrays
运算符时, valueForKeyPath:
创建并返回一个数组, 其中包含与由右键路径指定的属性相对应的所有集合的组合的所有对象, 而不删除重复项。
在arrayOfArrays
中的所有数组中获取payee
属性的值:
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
生成的collectedPayees
数组包含以下值:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital, General Cable
。
注意
@distinctUnionOfArrays
运算符类似, 但移除重复对象。
@distinctUnionOfSets
当指定@distinctUnionOfSets
运算符时, valueForKeyPath:
创建并返回一个NSSet
对象, 其中包含与由右键路径所指定的属性相对应的所有集合组合的不同对象。
此运算符的行为与@distinctUnionOfArrays
类似, 只是它需要一个NSSet
实例, 其中包含对象的NSSet
实例, 而不是NSArray
实例中包含NSArray
实例。此外, 它还返回一个NSSet
实例。假设示例数据已存储在集合而不是数组中, 则示例调用和结果与@distinctUnionOfArrays
中显示的相同。.
验证属性
键值编码协议定义了支持属性验证的方法。正如使用基于键的访问器读取和写入键值编码兼容对象的属性一样, 也可以按键 (或键路径) 验证属性。当您调用validateValue:forKey:error:
(或validateValue:forKeyPath:error:
) 方法时, 协议的默认实现将搜索接收验证消息的对象 (或在键路径的末尾的对象), 该方法的名称与模式validate<Key>:error:
相匹配。如果对象没有此类方法, 则默认情况下验证成功, 默认实现返回YES
.当存在属性特定的验证方法时, 默认实现将返回调用该方法的结果。
注意
您通常仅在Objective-C
中使用此处描述的验证。在 Swift 中, 通过依赖 optionals 和强类型检查的编译器支持, 可以更便捷地处理属性验证, 同时使用内置的 willSet 和 didSet 属性观察器来测试任何运行时 API 协定, 详见
The Swift Programming Language (Swift 4.1)
中Property Observers章节对willSet
didSet
的描述。
由于属性特定的验证方法通过引用的方式接收值和错误参数, 因此验证有三种可能的结果:
- 验证方法认为值对象有效并返回YES而不改变值或错误。
- 验证方法认为值对象无效, 但选择不更改它。在这种情况下, 该方法返回NO并将错误引用 (如果调用方提供) 设置为指示失败原因的NSError对象。
- 验证方法认为值对象无效, 但创建一个新的、有效的替换项。在这种情况下, 该方法返回YES同时使错误对象不被触及。返回之前, 该方法修改值引用以指向新值对象。当它进行修改时, 该方法总是创建一个新对象, 而不是修改旧值, 即使 value 对象是可变的。
清单 6-1显示了如何调用name
字符串的验证的示例。
Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
NSLog(@"%@",error);
}
自动验证
通常, 键值编码协议及其默认实现都不定义自动执行验证的任何机制。相反, 您可以在您的应用程序中使用适合的验证方法。
某些其他Cocoa技术在某些情况下会自动进行验证。例如, 当保存托管对象上下文时, Core Data
自动执行验证 (详见Core Data Programming Guide)。此外, 在 macOS 中, Cocoa Bindings
允许您指定验证是否自动发生 (请阅读Cocoa Bindings Programming Topics了解有关Cocoa Bindings的更多信息。)。
访问器搜索模式
NSObject
提供的NSKeyValueCoding
协议的默认实现将基于键的访问器调用映射到对象的基础属性, 使用一组明确定义的规则。这些协议方法使用键参数搜索其自己的对象实例, 以查找访问器、实例变量以及遵循某些命名约定的相关方法。尽管您很少修改此默认搜索, 但了解它的工作原理, 对于跟踪键值编码对象的行为以及使您自己的对象兼容是很有帮助的。
注意
本节中的描述使用<key >
或<key >
作为在一个键值编码协议方法中出现的作为参数的键串的占位符,然后该方法被用作二次方法调用或变量名称查找的一部分。映射的属性名称遵循占位符的情况。例如, 对于getter <key>
和is<Key>
, 名为 "hidden" 的属性映射为hidden
和isHidden
.
基本的Getter搜索模式
valueForKey:
的默认实现, 给定一个key
参数作为输入, 执行以下过程, 从接收valueForKey
调用的类实例中进行操作:。
在实例中搜索找到第一个访问器方法, 其名称类似于
get<Key>
、<key>
、is<Key>
或_<key>
, 按此顺序。如果找到, 则调用它, 然后继续执行步骤5以得到结果。否则继续执行下一步。-
如果找不到简单访问器方法, 则在实例中搜索其名称与模式
countOf<Key>
和objectIn<Key>AtIndex:
(对应于NSArray类中定义的基本方法)和<key>AtIndexes:
(对应于NSArray
类中的objectsAtIndexes:
方法)方法。如果第一个方法和至少其他两个方法中的一个方法被找到, 则创建一个能够响应所有
NSArray
方法的集合代理对象,并返回。否则, 请继续执行步骤3。代理对象随后将接收到的任何
NSArray
消息转换为countOf<Key>
、objectIn<Key>AtIndex:
和<key>AtIndexes:
消息的某些组合到创建它的键值编码兼容对象。如果原始对象还实现了一个名为 "get<Key>:range:
" 的可选方法, 则代理对象在适当时也会使用它。实际上, 与键值编码兼容对象一起工作的代理对象允许基础属性拥有像NSArray
对象一样的行为, 即使它不是。 -
如果找不到简单访问器方法或数组访问方法组, 则查找名为
countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>:
(对应NSSet
类中定义的三种基本方法)。如果三个方法找都被找到了, 则创建一个能够响应所有
NSSet
方法的集合代理对象,并返回。否则, 继续执行步骤4。此代理对象随后将其接收到的任何
NSSet
消息转换为countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>:
消息的某些组合到创建它的对象的中。实际上, 与键值编码兼容对象一起工作的代理对象允许基础属性拥有像NSSet
一样的行为, 即使它不是。 如果找不到简单访问器方法或集合访问方法组, 并且如果接收者的类方法
accessInstanceVariablesDirectly
返回YES
, 搜索名为_<key>
、_is<Key>
、<key>
或is<Key>
的实例变量, 按该顺序进行。如果找到, 则直接获取实例变量的值, 然后继续执行步骤5。否则, 继续执行步骤6。-
如果检索到的属性值是对象指针, 则只需返回结果。
如果该值是
NSNumber
所支持的标量类型, 则将其存储在NSNumber
实例中并返回。
如果结果是 NSNumber 不支持的标量类型, 则转换为NSValue
对象, 然后返回它。 如果所有其他操作都失败, 则调用
valueForUndefinedKey:
。默认情况下, 这会引发异常, 但NSObject
的子类可能会提供特定于键的行为。
基本的Setter搜索模式
setValue:forKey:
的默认实现, 给定key
和value
参数作为输入, 在接收调用的对象内尝试将名为key
的属性设置为value
(或者, 对于非对象属性, 则为unwarp value
, 详见表示非对象值) , 使用以下过程:
- 按照顺序查找第一个名为
set<Key>:
或_set<Key>
的访问器。如果找到, 用输入值 (或根据需要unwrap value
) 调用它, 然后完成。 - 如果找不到简单访问器, 并且类方法accessInstanceVariablesDirectly返回YES, 则查找具有_<key>、 _is<Key>、 <key>或is<Key>的名称的实例变量, 按该排列顺序。如果找到, 则直接使用输入值 (或
unwrap value
) 设置变量并完成。
在找不到访问器或实例变量时, 调用setValue:forUndefinedKey:。默认情况下, 这会引发异常, 但NSObject的子类可能会提供特定于键的行为。
可变数组的搜索模式
mutableArrayValueForKey:
的默认实现, 给定一个key
参数作为输入, 在接收访问器调用的对象内为名为key
的属性返回一个可变的代理数组, 使用以下过程:
-
查找一对方法,如
insertObject:in<Key>AtIndex:
和removeObjectFrom<Key>AtIndex:
(对应于NSMutableArray
的基本方法方法insertObject:atIndex:
和removeObjectAtIndex:
), 或insert<Key>:atIndexes:
和remove<Key>AtIndexes:
(对应于NSMutableArray
的insertObjects:atIndexes:
和removeObjectsAtIndexes:
方法)。如果对象有至少一个插入方法和至少一个删除方法, 则返回一个代理对象,该代理对象响应
NSMutableArray
消息,并将insertObject:in<Key>AtIndex:
,removeObjectFrom<Key>AtIndex:
,insert<Key>:atIndexes:
, andremove<Key>AtIndexes:
消息的一些组合发送给mutableArrayValueForKey:
的原始接受者。当接收
mutableArrayValueForKey:
消息的对象还实现一个可选的替换对象方法, 其名称类似于replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
, 代理对象在适合最佳性能时也会利用这些功能。 如果对象没有可变数组方法, 则改用其名称与模式set<Key>:相匹配的访问器方法。在这种情况下, 通过向
mutableArrayValueForKey:
的原始接收者发出set<Key>:
消息, 返回响应NSMutableArray
消息的代理对象。
注意
此步骤中描述的机制比上一步的效率要低得多, 因为它可能涉及重复创建新的集合对象, 而不是修改现有的。因此, 在设计自己的键值编码兼容对象时, 通常应避免这种情况。
-
如果未找到可变数组方法或访问器, 并且接收者的类响应
YES
对于accessInstanceVariablesDirectly
, 则按照该顺序搜索具有_<key>
或<key>
等名称的实例变量。如果找到了这样的实例变量, 则返回一个代理对象, 它将接收到的每个
NSMutableArray
消息转发给实例变量的值, 这通常是NSMutableArray
或其子类的实例。 -
如果所有其他操作失败, 则返回一个可变的集合代理对象, 它将发出
setValue:forUndefinedKey:
消息到mutableArrayValueForKey
的原始接收者, 每当它收到
NSMutableArray`消息。setValue:forUndefinedKey:
的默认实现会抛出NSUndefinedKeyException异常, 但子类可能会重写此行为。