1. 简介
1.1 关于KVC
键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议提供对其属性的间接访问。当一个对象与键值编码兼容时,它的属性可以通过一个简洁、统一的消息传递接口通过字符串参数寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。
通常使用访问器方法来访问对象的属性。get访问器(或getter)返回属性的值。set访问器(或setter)设置属性的值。在Objective-C中,还可以直接访问属性的底层实例变量。以上述任何一种方式访问对象属性都很简单,但需要调用特定于属性的方法或变量名。随着属性列表的增长或更改,访问这些属性的代码也必须如此。相反,一个与键值编码兼容的对象提供了一个简单的消息传递接口,该接口在其所有属性中都是一致的。
键值编码是许多其他Cocoa技术的基础概念,例如KVO、Cocoa绑定、Core Data和AppleScript。在某些情况下,键值编码也有助于简化代码。
使用键值编码兼容对象
对象从NSObject(直接或间接)继承时通常采用键值编码,NSObject既采用NSKeyValueCoding协议,又为基本方法提供默认实现。此类对象使其他对象能够通过压缩消息传递接口执行以下操作:
-
访问对象属性。协议指定了一些方法,例如泛型gettervalueForKey:和泛型settersetValue:for key:,用于按名称或键访问对象属性,并将其参数化为字符串。这些方法和相关方法的默认实现使用键来定位底层数据并与之交互,如访问对象属性中所述。
-
操作集合属性。访问方法的默认实现与任何其他属性一样,使用对象的集合属性(如NSArray对象)。此外,如果对象为属性定义了集合访问器方法,则它将启用对集合内容的键值访问。这通常比直接访问更有效,并允许您通过标准化接口使用自定义集合对象。
-
对集合对象调用集合运算符。在键值编码兼容对象中访问集合属性时,可以将集合运算符插入到键字符串中,如使用集合运算符中所述。集合运算符默认的NSKeyValueCoding getter实现对集合执行操作,然后返回集合的新筛选版本或表示集合某些特征的单个值。
-
访问非对象属性。协议的默认实现检测非对象属性,包括标量和结构,并自动将它们作为对象包装和展开,以便在协议接口上使用。此外,协议声明了一个方法,当通过键值编码接口在非对象属性上设置nil值时,允许兼容对象为这种情况提供合适的操作。
-
按key path访问属性。当您有一个与键值编码兼容的对象的层次结构时,您可以使用基于键值路径的方法调用来向下钻取、获取或设置层次结构中使用单个调用的深层值。
对象采用键值编码
为了使您自己的对象键值编码兼容,您需要确保它们采用NSKeyValueCoding非正式协议并实现相应的方法,例如value forKey:
和setValue:forKey:
。幸运的是,NSObject采用此协议并为这些方法和其他基本方法提供默认实现。因此,如果从NSObject(或其许多子类中的任何一个子类)派生对象,那么大部分工作已经为您完成了。
为了让默认方法完成它们的工作,需要确保对象的访问器方法和实例变量遵循某些定义良好的模式。这允许默认实现查找对象的属性以响应键值编码的消息。然后,您可以选择通过提供用于验证和处理某些特殊情况的方法来扩展和自定义键值编码。
用Swift进行键值编码
默认情况下,继承自NSObject或其子类之一的Swift对象是符合其属性的键值编码。在Objective-C中,属性的访问器和实例变量必须遵循某些模式,而Swift中的标准属性声明会自动保证这一点。另一方面,协议的许多功能要么不相关,要么使用Objective-C中不存在的本地Swift构造或技术更好地处理。例如,由于所有Swift属性都是对象,因此您永远不会对非对象属性执行默认实现的特殊处理。
因此,虽然键值编码协议方法直接转换为Swift,但本指南主要关注Objective-C,在这里您需要做更多的工作来确保遵从性,并且键值编码通常最有用。需要在Swift中采用显著不同方法的情况在本指南中均有说明。
有关将Swift与Cocoa技术结合使用的更多信息,请阅读将Swift与Cocoa和Objective-C结合使用(Swift 3)。有关Swift的完整描述,请阅读Swift编程语言(Swift 3)。
其他可可技术依赖于关键值编码
符合关键值编码的对象可以参与各种依赖于这种访问的Cocoa技术,包括:
-
KVO。此机制使对象能够注册由另一个对象的属性更改驱动的异步通知。
-
Cocoa 绑定。这些技术的集合完全实现了MVC范式,其中模型封装应用程序数据,视图显示和编辑数据,控制器在两者之间进行中介。
-
Core Data。该框架为与对象生命周期和对象图管理(包括持久性)相关联的常见任务提供了通用和自动化的解决方案。您可以阅读核心数据编程指南中的核心数据。
-
AppleScript。这种脚本语言可以直接控制可脚本化的应用程序和macOS的许多部分。Cocoa的脚本支持利用键值编码来获取和设置可脚本对象中的信息。NSScriptKeyValueCoding非正式协议中的方法提供了处理键值编码的附加功能,包括通过多值键中的索引获取和设置键值,以及将键值强制(或转换)为适当的数据类型。AppleScript概述提供了AppleScript及其相关技术的高级概述。
2. KVC基础
2.1访问对象属性
对象通常在其接口声明中指定属性,这些属性属于以下类别之一:
-
属性。这些是简单的值,例如标量、字符串或布尔值。值对象(如NSNumber)和其他不可变类型(如NSColor)也被视为属性。
-
一对一关系。这些是具有自身属性的可变对象。对象的属性可以在不更改对象本身的情况下更改。例如,银行帐户对象可能具有所有者属性,该属性是Person对象的实例,而Person对象本身具有地址属性。业主的地址可在不更改银行账户所持业主证明的情况下更改。银行帐户的所有者没有改变。只有他们的地址。
-
一对多关系。这些是集合对象。您通常使用NSArray或NSSet的实例来保存此类集合,但也可以使用自定义集合类。
@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
为了维护封装,对象通常为其接口上的属性提供访问器方法。对象的作者可以显式地编写这些方法,也可以依赖编译器自动合成它们。无论如何,使用这些访问器之一的代码的作者必须在编译代码之前将属性名写入代码。访问器方法的名称成为使用它的代码的静态部分。例如,BankAccount
对象,编译器将合成一个setter,您可以为myAccount
实例调用它:
[myAccount setCurrentBalance:@(100.0)];
这是直接的,但缺乏灵活性。另一方面,与键值编码兼容的对象提供了一种更通用的机制来使用字符串标识符访问对象的属性。
用键和键路径标识对象的属性
键是标识特定属性的字符串。通常,根据约定,表示属性的键是在代码中出现的属性本身的名称。密钥必须使用ASCII编码,不能包含空格,并且通常以小写字母开头(尽管有例外,例如在许多类中找到的URL属性)。
因为BankAccount
类是符合键值编码的,所以它可以识别出owner
、currentBalance
和transactions
,这是它的属性名。您不必调用setCurrentBalance:
方法,而是可以通过其键设置值:
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
实际上,可以使用不同的键参数,用同一方法设置myAccount
对象的所有属性。因为参数是字符串,所以它可以是在运行时操作的变量。
键路径是.
分隔键的字符串,用于指定要遍历的对象属性序列。序列中第一个键的属性是相对于接收器的,并且每个后续键都是相对于前一个属性的值计算的。键路径对于使用单个方法调用向下钻取对象的层次结构非常有用。
例如,应用于银行帐户实例的键路径owner.address.street
引用存储在银行帐户所有者地址中的街道字符串的值,假设Person和address类也与键值编码兼容。
使用键获取属性值
当一个对象采用NSKeyValueCoding协议时,它是与键值编码兼容的。一个继承自NSObject的对象,它提供了协议基本方法的默认实现,自动采用该协议并具有某些默认行为。这样的对象至少实现以下基本的基于键的getter:
-
valueForKey:返回由键参数命名的属性的值。如果根据访问器搜索模式中描述的规则找不到由键命名的属性,则对象会向自身发送一个
valueForUndefinedKey:message
。valueForUndefinedKey
的默认实现:引发一个NSUndefinedKeyException
,但是子类可以重写此行为并更优雅地处理这种情况。 -
valueForKeyPath:返回相对于接收器的指定键路径的值。键路径序列中不符合特定键的键值编码的任何对象,即valueForKey的默认实现找不到访问器方法,会发出
valueForUndefinedKey:
消息。 -
dictionaryWithValuesForKeys:返回相对于接收器的键数组的值。该方法为数组中的每个键调用
valueForKey:
。返回的NSDictionary包含数组中所有键的值。
集合对象(如NSArray、NSSet和NSDictionary)不能包含nil作为值。相反,可以使用NSNull对象表示nil值。NSNull提供一个实例,表示对象属性的nil值。dictionaryWithValuesForKeys:和相关的setValuesForKeysWithDictionary:自动在NSNull(在dictionary参数中)和nil(在存储的属性中)之间转换。
当使用键路径来寻址属性时,如果键路径中除最后一个键之外的任何键是多对多关系(即,它引用一个集合),则返回值是一个集合,其中包含多对多键右侧的键的所有值。例如,请求键路径transactions.payee
的值将返回一个包含所有事务的所有受款人对象的数组。这也适用于键路径中的多个数组。键路径accounts.transactions.payee
返回一个数组,其中包含所有帐户中所有事务的所有受款人对象。
使用键设置属性值
与getter一样,与键值编码兼容的对象还根据NSObject中的NSKeyValueCoding协议的实现,通用setter提供默认行为:
-
setValue:forKey: 将指定键相对于接收消息的对象的值设置为给定值。
setValue:forKey:
的默认实现:自动展开表示标量和结构的NSNumber和NSValue对象,并将它们分配给属性。如果指定的键对应于接收setter调用的对象不具有的属性,则该对象会向自身发送
setValue:forUndefinedKey:
消息。setValue:forUndefinedKey:
的默认实现引发了NSUndefinedKeyException。但是,子类可以重写此方法以自定义方式处理请求。 -
setValue:forKeyPath: 在相对于接收器的指定键路径处设置给定值。键路径序列中不符合特定键的键值编码的任何对象都将接收
setValue:forUndefinedKey:
消息。 -
setValuesForKeysWithDictionary: 使用指定字典中的值设置接收器的属性,使用字典键标识属性。默认实现为每个键值对调用
setValue:forFey:
,根据需要用nil替换NSNull对象。
在默认实现中,当您试图将非对象属性设置为nil值时,与键值编码兼容的对象会向自己发送一条setNilValueForKey:
消息。setNilValueForKey
的默认实现:引发NSInvalidArgumentException,但对象可以重写此行为以替换默认值或标记值。
使用键简化对象访问
要了解基于键的getter和setter如何简化代码,请考虑以下示例。在macOS中,NSTableView和NSOutlineView对象将标识符字符串与它们的每一列相关联。如果支持表的模型对象不符合键值编码,则表的数据源方法将被迫依次检查每个列标识符,以找到要返回的正确属性。此外,在将来向模型中添加另一个属性(在本例中为Person对象)时,还必须重新访问数据源方法,添加另一个条件以测试新属性并返回相关值。
- (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;
}
下面的代码为同一个数据源方法体统了一个更加紧凑的实现。该方法利用了一个与键值编码兼容的Person对象。仅使用valueForKey:
getter,数据源方法使用列标识符作为键返回适当的值。除了更短之外,它还更通用,因为在以后添加新列时,只要列标识符始终与模型对象的属性名称匹配,它将继续保持不变的工作。
- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
return [[self.people objectAtIndex:row] valueForKey:[column identifier]];
}
2.2 访问集合属性
可以像使用valueForKey:
和setValue:forKey:
(或它们的等价键路径)一样获取或设置集合对象。但是,当您想要操作这些集合的内容时,通常使用协议定义的可变代理方法是最有效的。
协议为集合对象访问定义了三种不同的代理方法,每种方法都有一个键和一个键路径变量:
-
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
它们返回一个代理对象,其行为类似于NSMutableArray对象。
-
mutableSetValueForKey:
和mutableSetValueForKeyPath:
它们返回一个代理对象,其行为类似于NSMutableSet对象。
-
mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
它们返回一个代理对象,其行为类似于NSMutableOrderedSet对象。
在代理对象上操作、向其中添加对象、从中删除对象或替换其中的对象时,协议的默认实现会相应地修改基础属性。这比使用valueForKey:
获取不可变的集合对象、使用更改的内容创建已修改的集合对象,然后使用setValue:forKey:
消息将其存储回对象更有效。在许多情况下,它也比直接使用可变属性更有效。这些方法还提供了一个额外的好处,即为collection对象中保存的对象保持键值观测遵从性。
2.3 使用集合运算符
当发送符合键值编码的对象valueForKeyPath:
消息时,可以在键值路径中嵌入集合运算符。集合运算符是前面有at符号(@)的一个小关键字列表,该符号指定getter在返回数据之前应执行的操作,以便以某种方式操作数据。由NSObject提供的valueForKeyPath
的默认实现实现了此行为。
当键路径包含集合运算符时,该运算符之前的键路径的任何部分(称为左键路径)指示相对于消息的接收者要对其进行操作的集合。如果将消息直接发送到集合对象(如NSArray实例),则可以省略左键路径。
运算符后面的键路径部分(称为右键路径)指定运算符应处理的集合内的属性。除@count
之外的所有集合运算符都需要一个正确的键路径。
集合运算符显示三种基本类型的行为:
-
聚合运算符以某种方式合并集合的对象,并返回通常与右键路径中命名的属性的数据类型匹配的单个对象。
@count
运算符是一个例外,它不接受右键路径,并且总是返回NSNumber实例。 -
数组运算符返回一个NSArray实例,该实例包含命名集合中保存的某些对象子集。
-
嵌套运算符处理包含其他集合的集合,并返回NSArray或NSSet实例(取决于运算符),该实例以某种方式组合嵌套集合的对象。
样本数据
示例数据后面的描述包括演示如何调用每个运算符的代码片段,以及调用的结果。BankAccount
类,该类包含一个Transaction
对象数组。每一个都代表一个简单的支票簿条目。
@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
@interface Transaction : NSObject
@property (nonatomic) NSString* payee; // To whom
@property (nonatomic) NSNumber* amount; // How much
@property (nonatomic) NSDate* date; // When
@end
为了便于讨论,假设BankAccount
实例有一个用下表所示数据填充的事务数组,并且从BankAccount
对象内部进行了示例调用。
payee values |
amount values formatted as currency |
date values formatted as month day, year |
---|---|---|
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 |
聚合运算符
聚合运算符处理一个数组或一组属性,生成反映集合某个方面的单个值。
@avg
指定@avg
运算符时,valueForKeyPath:
读取由集合中每个元素的右键路径指定的属性,将其转换为double
(用0替换nil值),并计算这些值的算术平均值。然后返回存储在NSNumber实例中的结果。
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
//结果为transactionAverage = $456.54
@count
指定@count
运算符时,valueForKeyPath:
返回NSNumber实例中集合中的对象数。右键路径(如果存在)将被忽略。
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
//结果为numberOfTransactions = 13
@max
指定@max
运算符时,valueForKeyPath:
在由右键路径命名的集合项中搜索并返回最大的一个。搜索使用compare:
方法进行比较,该方法由许多基础类(如NSNumber类)定义。因此,右键路径指示的属性必须包含对该消息有意义响应的对象。搜索将忽略值为零的集合项。
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
//结果为latestDate = Jul 15, 2016.
@min
指定@min
运算符时,valueForKeyPath:
在由右键路径命名的集合项中搜索并返回最小的集合项。搜索使用compare:
方法进行比较,该方法由许多基础类(如NSNumber类)定义。因此,右键路径指示的属性必须包含对该消息有意义响应的对象。搜索将忽略值为零的集合项。
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
//结果为earliestDate = Dec 1, 2015.
@sum
指定@sum
运算符时,valueForKeyPath:
读取由集合中每个元素的右键路径指定的属性,将其转换为double(用0替换nil值),并计算这些值的和。然后返回存储在NSNumber实例中的结果。
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
//结果为amountSum = $5,935.00
数组运算符
数组运算符导致valueForKeyPath:
返回一个对象数组,该数组对应于由右键路径指示的特定对象集。
注意:使用数组运算符时,如果任何叶对象为零,valueForKeyPath:方法将引发异常。
@distinctUnionOfObjects
指定@distinctUnionOfObjects
运算符时,valueForKeyPath:
创建并返回一个数组,该数组包含与右键路径指定的属性相对应的集合的不同对象。
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
//结果为[Car Loan, General Cable, Animal Hospital, Green Power, Mortgage]
@unionOfObjects
指定@unionOfObjects
运算符时,valueForKeyPath:
创建并返回一个数组,该数组包含与右键路径指定的属性相对应的集合的所有对象。与@distinctUnionOfObjects
不同,不会删除重复的对象。
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
//结果为[Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital]
嵌套运算符
嵌套运算符对嵌套集合进行操作,集合的每个项本身都包含一个集合。
注意:使用嵌套运算符时,如果任何叶对象为零,valueForKeyPath:方法将引发异常。
对于下面的描述,请考虑另一个名为moreTransactions
的数据数组,该数组由下表中的数据填充,并与原始事务数组一起收集到一个嵌套数组中:
NSArray* moreTransactions = @[<# transaction data #>];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
payee values |
amount values formatted as currency |
date values formatted as month day, year |
---|---|---|
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 Shop |
$600.00 |
Jun 14, 2016 |
@distinctUnionOfArrays
指定@distinctUnionOfArrays
运算符时,valueForKeyPath:
创建并返回一个数组,该数组包含与右键路径指定的属性相对应的所有集合组合的不同对象。
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
//结果为[Hobby Shop, Mortgage, Animal Hospital, Second Mortgage, Car Loan, General Cable - Cottage, General Cable, Green Power]
@unionOfArrays
指定@unionOfArrays
运算符时,valueForKeyPath:
创建并返回一个数组,该数组包含与右键路径指定的属性相对应的所有集合组合的所有对象,而不删除重复项。
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
//结果为[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 - Cottage, General Cable - Cottage, General Cable - Cottage, Second Mortgage, Second Mortgage, Second Mortgage, Hobby Shop]
@distinctUnionOfSets
指定@distinctUnionOfSets
运算符时,valueForKeyPath:
创建并返回一个NSSet对象,该对象包含与右键路径指定的属性相对应的所有集合的组合的不同对象。
此运算符的行为与@distinctUnionOfArrays
类似,只是它希望NSSet实例包含对象的NSSet实例,而不是NSArray实例的NSArray实例。此外,它还返回一个NSSet实例。假设示例数据存储在集合而不是数组中,则示例调用和结果与@distinctUnionOfArrays
中显示的调用和结果相同。
2.4 表示非对象值
NSObject提供的键值编码协议方法的默认实现同时使用对象和非对象属性。默认实现在对象参数或返回值与非对象属性之间自动转换。这允许基于键的getter和setter的签名保持一致,即使存储的属性是标量或结构。
注意:因为Swift中的所有属性都是对象,所以本节只适用于Objective-C属性。
当您调用协议的一个getter(如valueForKey:
)时,默认实现将根据访问器搜索模式中描述的规则确定为指定键提供值的特定访问器方法或实例变量。
如果返回值不是对象,则getter使用此值初始化NSNumber对象(用于标量)或NSValue对象(用于结构)并返回该值。
类似地,在默认情况下,setValue:forKey:
这样的setter确定给定特定键的属性访问器或实例变量所需的数据类型。如果数据类型不是对象,则setter首先向传入的Value对象发送适当的<type>Value
消息以提取底层数据,并存储该数据。
注意:当您使用非对象属性的nil值调用一个键值编码协议setter时,setter没有明显的、通用的操作过程。因此,它向接收setter调用的对象发送setNilValueForKey:消息。此方法的默认实现引发NSInvalidArgumentException异常,但子类可能重写此行为,如处理非对象值(例如设置标记值或提供有意义的默认值)中所述。
包装和展开标量类型
下表列出了默认键值编码实现使用NSNumber实例包装的标量类型。对于每个数据类型,该表显示了用于从基础属性值初始化NSNumber以提供getter返回值的创建方法。然后显示在set操作期间用于从setter输入参数提取值的访问器方法。
Data type | Creation method | Accessor method |
---|---|---|
BOOL |
numberWithBool: |
boolValue (in iOS)charValue (in macOS)* |
char |
numberWithChar: |
charValue |
double |
numberWithDouble: |
doubleValue |
float |
numberWithFloat: |
floatValue |
int |
numberWithInt: |
intValue |
long |
numberWithLong: |
longValue |
long long |
numberWithLongLong: |
longLongValue |
short |
numberWithShort: |
shortValue |
unsigned char |
numberWithUnsignedChar: |
unsignedChar |
unsigned int |
numberWithUnsignedInt: |
unsignedInt |
unsigned long |
numberWithUnsignedLong: |
unsignedLong |
unsigned long long |
numberWithUnsignedLongLong: |
unsignedLongLong |
unsigned short |
numberWithUnsignedShort: |
unsignedShort |
注意:*在macOS中,由于历史原因,BOOL被定义为带符号的char类型,KVC不区分这两种类型。因此,当键为BOOL时,不应将诸如@“true”或@“YES”之类的字符串值传递给setValue:forKey:。KVC将尝试调用charValue(因为BOOL本身就是一个char),但是NSString没有实现这个方法,这会导致运行时错误。相反,当键是BOOL时,只将NSNumber对象(如@(1)或@(YES))作为值参数传递给setValue:forKey:。此限制不适用于iOS,在iOS中,BOOL是定义为本机布尔类型BOOL的类型,KVC调用boolValue,它适用于NSNumber对象或格式正确的NSString对象。
包装和拆封结构
下表显示了默认访问器用于包装和展开常用NSPoint、NSRange、NSRect和NSSize结构的创建和访问器方法。
Data type | Creation method | Accessor method |
---|---|---|
NSPoint |
valueWithPoint: |
pointValue |
NSRange |
valueWithRange: |
rangeValue |
NSRect |
valueWithRect: (macOS only). |
rectValue |
NSSize |
valueWithSize: |
sizeValue |
自动包装和展开不限于NSPoint、NSRange、NSRect和NSSize。结构类型(即Objective-C类型编码字符串以{开头的类型)可以包装在NSValue对象中。
typedef struct {
float x, y, z;
} ThreeFloats;
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
使用这个类的一个名为myClass
的实例,您可以获得带有键值编码的threeFloats值:
NSValue* result = [myClass valueForKey:@"threeFloats"];
valueForKey
的默认实现:调用threeFloats
getter,然后返回包装在NSValue对象中的结果。
类似地,可以使用键值编码设置threeFloats值:
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];
默认实现使用getValue:
消息来展开值,然后使用结果结构调用setThreeFloats:
。
2.5 验证属性
键值编码协议定义了支持属性验证的方法。和使用基于键的访问器来读写符合键值编码的对象的属性一样,也可以通过键(或键路径)来验证属性。调用validateValue:forKey:error:
(或validateValue:forKeyPath:error:
)方法时,协议的默认实现将搜索接收验证消息的对象(或键路径末尾的对象),以查找名称与模式validate<Key>:error:
匹配的方法。如果对象没有此类方法,则默认情况下验证成功,默认实现返回YES。如果存在特定于属性的验证方法,则默认实现将返回调用该方法的结果。
注意:您通常只使用Objective-C中描述的验证。在Swift中,属性验证更习惯于通过依赖编译器对选项和强类型检查的支持来处理,而使用内置的willSet和didSet属性观察程序来测试任何运行时API协定,如Swift编程语言(Swift 3)。
由于特定于属性的验证方法通过引用接收值和错误参数,因此验证有三种可能的结果:
- 验证方法认为值对象有效,并在不更改值或错误的情况下返回YES。
- 验证方法认为值对象无效,但选择不更改它。在这种情况下,该方法返回NO并将错误引用(如果由调用方提供)设置为指示失败原因的NSError对象。
- 验证方法认为值对象无效,但创建一个新的有效对象作为替换。在这种情况下,该方法返回YES,同时保持错误对象不变。在返回之前,该方法修改值引用以指向新的值对象。当进行修改时,方法总是创建一个新对象,而不是修改旧对象,即使值对象是可变的。
Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
NSLog(@"%@",error);
}
自动验证
通常,键值编码协议及其默认实现都不定义任何自动执行验证的机制。相反,您可以在适合您的应用程序时使用验证方法。
某些其他Cocoa技术在某些情况下会自动执行验证。例如,在保存托管对象上下文时,核心数据会自动执行验证。此外,在macOS中,Cocoa绑定允许您指定验证应自动进行。
2.6 访问器搜索模式
NSObject提供的NSKeyValueCoding协议的默认实现使用一组明确定义的规则将基于键的访问器调用映射到对象的底层属性。这些协议方法使用一个键参数来搜索它们自己的对象实例,以查找访问器、实例变量和遵循某些命名约定的相关方法。尽管很少修改此默认搜索,但了解它的工作原理可能会有所帮助,这既有助于跟踪键值编码对象的行为,也有助于使您自己的对象兼容。
注意:本节中的描述使用<key>或<key>作为键字符串的占位符,该键字符串在一个键值编码协议方法中显示为参数,然后该方法将其用作辅助方法调用或变量名查找的一部分。映射的属性名符合占位符的大小写。例如,对于getter<key>和is<key>,名为hidden的属性映射到hidden和ishiden。
基本Getter的搜索模式
给定一个键参数作为输入,valueForKey:
的默认实现从接收valueForKey:
调用的类实例中执行以下过程。
-
在实例中搜索找到的第一个访问器方法,该方法的名称如
get<Key>
、<Key>
、is<Key>
或_<Key>
。如果找到,则调用它并继续执行步骤5并返回结果。否则继续下一步。 -
如果找不到简单的访问器方法,则在实例中搜索名称与模式
countOf<Key>
和objectIn<Key>AtIndex:
(对应于NSArray类定义的基元方法)和<Key>AtIndex:
(对应于NSArray方法objectsAtIndexes:
)匹配的方法。如果找到其中的第一个和其他两个方法中的至少一个,则创建一个响应所有NSArray方法的集合代理对象并返回该对象。否则,继续执行步骤3。
代理对象随后将其接收到的任何NSArray消息转换为
countOf<Key>
、objectIn<Key>AtIndex:
和<Key>AtIndexes:
消息的组合,并将其转换为创建该对象的键值编码兼容对象。如果原始对象还实现了一个名为get:range:
的可选方法,则代理对象也会在适当时使用该方法。实际上,代理对象与键值编码兼容对象一起工作,允许底层属性的行为如同它是NSArray,即使它不是NSArray。 -
如果找不到简单的访问器方法或数组访问方法组,则查找名为
countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>
的三个方法(对应于NSSet类定义的基本方法)。如果找到这三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回该对象。否则,继续执行步骤4。
此代理对象随后将接收到的任何NSSet消息转换为
countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>:
消息的组合,并将其转换为创建它的对象。实际上,代理对象与键值编码兼容对象一起工作,允许底层属性的行为如同它是NSSet,即使它不是NSSet。 -
如果找不到简单的访问器方法或集合访问方法组,并且如果接收方的类方法
accessInstanceVariablesDirectly
返回YES,则按该顺序搜索名为_<key>
、_is<key>
、<key>
或is<key>
的实例变量。如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤6。 -
如果检索到的属性值是对象指针,则只需返回结果。
如果该值是NSNumber支持的标量类型,请将其存储在NSNumber实例中并返回该实例。
如果结果是NSNumber不支持的标量类型,则转换为NSValue对象并返回该对象。
-
如果所有其他操作都失败,请调用
valueForUndefinedKey:
。默认情况下,这会引发异常,但NSObject的子类可能提供特定于键的行为。
基本Setter的搜索模式
setValue:forKey:
的默认实现是,给定键和值参数作为输入,尝试在接收调用的对象内将名为key的属性设置为value(或者,对于非对象属性,使用以下过程将值的未包装版本设置为value,如表示非对象值中所述):
- 按此顺序查找第一个名为
set<Key>
或_set<Key>
的访问器。如果找到,使用输入值(或者根据需要使用unwrapped值)调用它并完成。 - 如果找不到简单的访问器,并且类方法
accessInstanceVariablesDirectly
返回YES,则按该顺序查找名为_<key>
、_is<key>
、<key>
或is<key>
的实例变量。如果找到,直接用输入值(或未包装值)设置变量并完成。 - 在找不到访问器或实例变量时,调用
setValue:forUndefinedKey:
。默认情况下,这会引发异常,但NSObject的子类可能提供特定于键的行为。
可变数组的搜索模式
mutableArrayValueForKey:
的默认实现是,给定一个键参数作为输入,使用以下过程返回接收访问器调用的对象内名为key的属性的可变代理数组:
- 查找一对名为
insertObject:in<Key>AtIndex:
和removeObjectFrom<Key>AtIndex:
的方法:(分别对应于NSMutableArray原语方法insertObject:atIndex:
和removeObjectAtIndex:
),或者方法名为insert<Key>:atIndexes:
和remove<Key>AtIndexes:
(对应于NSMutableArrayinsertObjects:atIndexes:
和removeObjectsAtIndexes:
方法)。
如果对象至少有一个插入方法和至少一个删除方法,则返回一个代理对象,该对象通过发送insertObject:in<Key>AtIndex:
、removeObjectFrom<Key>AtIndex:
、insert<Key>:AtIndex:
、remove<Key>AtIndex:
消息的组合来响应NSMutableArraymutableArrayValueForKey:
消息。
当接收mutableArrayValueForKey:
消息的对象还实现了一个可选的replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
,代理对象也会在适当的情况下使用这些方法以获得最佳性能。
-
如果对象没有可变数组方法,则改为查找名称与模式集
set<Key>:
匹配的访问器方法。在这种情况下,返回一个代理对象,该对象通过向mutableArrayValueForKey:
的原始接收方发出set<Key>:
消息来响应NSMutableArray消息。注意:此步骤中描述的机制比上一步的效率低得多,因为它可能涉及重复创建新的集合对象,而不是修改现有的集合对象。因此,在设计自己的键值编码兼容对象时,通常应该避免使用它。
-
如果既没有找到可变数组方法,也没有找到访问器,并且如果接收者的类直接响应
accessInstanceVariables
返回Yes,则按该顺序搜索名为_<key>
或<key>
的实例变量。如果找到这样的实例变量,则返回一个代理对象,该对象将接收到的每个NSMutableArray消息转发给实例变量的值,该值通常是NSMutableArray的实例或其子类之一。
-
如果所有其他操作都失败,则在
mutableArrayValueForKey:
消息接收到NSMutableArray消息时,将发出setValue:forUndefinedKey:
消息的可变集合代理对象返回给mutableArrayValueForKey:
消息的原始接收方。setValue:forUndefinedKey:
的默认实现引发了NSUndefinedKeyException,但子类可能会重写此行为。
可变序集的搜索模式
mutableOrderedSetValueForKey:
的默认实现:识别与valueForKey:
相同的简单访问器方法和有序集访问器方法:(参见基本Getter的默认搜索模式),并遵循相同的直接实例变量访问策略,但始终返回可变集合代理对象,而不是valueForKey:
返回的不可变集合。此外,它还执行以下操作:
-
查找一对名为
insertObject:in<Key>AtIndex:
和removeObjectFrom<Key>AtIndex:
的方法:(分别对应于NSMutableOrderedSet原语方法insertObject:atIndex:
和removeObjectAtIndex:
),或者方法名为insert<Key>:atIndexes:
和remove<Key>AtIndexes:
(对应于NSMutableOrderedSetinsertObjects:atIndexes:
和removeObjectsAtIndexes:
方法)。如果对象至少有一个插入方法和至少一个删除方法,则返回一个代理对象,该对象通过发送
insertObject:in<Key>AtIndex:
、removeObjectFrom<Key>AtIndex:
、insert<Key>:AtIndex:
、remove<Key>AtIndex:
消息的组合来响应NSMutableOrderedSetmutableOrderedSetValueForKey:
消息。当接收
mutableOrderedSetValueForKey:
消息的对象还实现了一个可选的replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
,代理对象也会在适当的情况下使用这些方法以获得最佳性能。 -
如果对象没有可变数组方法,则改为查找名称与模式集
set<Key>:
匹配的访问器方法。在这种情况下,返回一个代理对象,该对象通过向mutableOrderedSetValueForKey:
的原始接收方发出set<Key>:
消息来响应NSMutableOrderedSet消息。注意:此步骤中描述的机制比上一步的效率低得多,因为它可能涉及重复创建新的集合对象,而不是修改现有的集合对象。因此,在设计自己的键值编码兼容对象时,通常应该避免使用它。
-
如果既没有找到可变数组方法,也没有找到访问器,并且如果接收者的类直接响应
accessInstanceVariables
返回Yes,则按该顺序搜索名为_<key>
或<key>
的实例变量。如果找到这样的实例变量,则返回一个代理对象,该对象将接收到的每个NSMutableOrderedSet消息转发给实例变量的值,该值通常是NSMutableOrderedSet的实例或其子类之一。
-
如果所有其他操作都失败,则在
mutableOrderedSetValueForKey:
消息接收到NSMutableOrderedSet消息时,将发出setValue:forUndefinedKey:
消息的可变集合代理对象返回给mutableOrderedSetValueForKey:
消息的原始接收方。setValue:forUndefinedKey:
的默认实现引发了NSUndefinedKeyException,但子类可能会重写此行为。
可变集的搜索模式
mutableSetValueForKey:
的默认实现是,给定一个键参数作为输入,使用以下过程返回接收访问器调用的对象内名为key的数组属性的可变代理集:
-
搜索名为
add<Key>Object:
和remove<Key>Object:
(分别对应于NSMutableSetaddObject:
和removeObject:
)的方法,以及add<Key>:
和remove<Key>:
(对应于NSMutableSetunionSet:
和minusSet:
)。如果至少找到一个添加方法和至少一个删除方法,则返回一个代理对象,该代理对象将向
mutableSetValueForKey:
的原始接收者发送add<Key>Object:
、remove<Key>Object:
、add<Key>:
和remove:
消息的组合,用于接收的每个NSMutableSet消息。代理对象还使用
intersect<Key>:
或set<Key>:
等名称的方法以获得最佳性能(如果它们可用)。 -
如果
mutableSetValueForKey:
调用的接收者是托管对象,则搜索模式不会像对非托管对象那样继续。有关详细信息,请参阅Core Data Programming Guide中的托管对象访问器方法。 -
如果找不到可变集方法,并且该对象不是托管对象,则搜索名称类似
set<Key>:
的访问器方法。如果找到这样的方法,则返回的代理对象将为其接收的每个NSMutableSet消息向mutableSetValueForKey:
的原始接收方发送set<Key>:
消息。注意:此步骤中描述的机制比第一步效率低得多,因为它可能涉及重复创建新的集合对象,而不是修改现有的集合对象。因此,在设计自己的键值编码兼容对象时,通常应该避免使用它。
-
如果找不到可变集方法和访问器方法,并且如果
accessInstanceVariablesDirectly
类方法返回YES,则按该顺序搜索名称类似于_<key>
或<key>
的实例变量。如果找到这样的实例变量,则代理对象将收到的每个NSMutableSet消息转发给实例变量的值,该值通常是NSMutableSet的实例或其子类之一。 -
如果所有其他操作都失败,则返回的代理对象将通过向
mutableSetValueForKey:
的原始接收方发送setValue:forUndefinedKey:
消息来响应它接收到的任何NSMutableSet消息。
3. KVC的应用
3.1 实现基本的键值编码
当为对象采用键值编码时,您依赖于NSKeyValueCoding协议的默认实现,方法是让您的对象继承自NSObject或其许多子类之一。而默认实现则依赖于您按照某些定义良好的模式定义对象的实例变量(或ivar)和访问器方法,以便它在接收到键值编码的消息(如valueForKey:
和setValue:forKey:
)时可以将键字符串与属性相关联。
您通常遵循Objective-C中的标准模式,只需使用@property
语句声明一个属性,并允许编译器自动合成ivar
和访问器。默认情况下,编译器遵循预期的模式。
注意:在Swift中,只需以通常的方式声明一个属性,就会自动生成适当的访问器,而且您永远不会直接与ivar交互。有关Swift中属性的更多信息,请阅读Swift编程语言(Swift 3)中的属性。有关从Swift与Objective-C属性交互的特定信息,请阅读使用Swift与Cocoa和Objective-C(Swift 3)中的Accessing properties。
如果您确实需要在Objective-C中手动实现访问器或ivar
,请遵循本节中的指南以保持基本的遵从性。
-
要提供增强与任何语言中的对象集合属性交互的附加功能,请实现定义集合方法中描述的方法。
-
要使用键值验证进一步增强对象,请实现添加验证中描述的方法。
注意:键值编码的默认实现使用比这里描述的更广泛的ivar和访问器。如果有使用其他变量或访问器约定的遗留代码,请检查访问器搜索模式中的搜索模式,以确定默认实现是否可以找到对象的属性。
基本的Getter
要实现返回属性值的getter,同时可能还要执行其他自定义工作,请使用与该属性类似的方法,例如对于title字符串属性:
- (NSString*)title
{
// Extra getter logic…
return _title;
}
对于包含布尔值的属性,也可以使用前缀为is的方法,例如对于隐藏的布尔属性:
- (BOOL)isHidden
{
// Extra getter logic…
return _hidden;
}
当属性是标量或结构时,键值编码的默认实现将值包装在对象中,以便在协议方法的接口上使用。你不需要做任何特别的事情来支持这种行为。
基本的Setter
要实现存储属性值的setter,请使用以单词set为前缀的属性大写名称的方法。对于隐藏属性:
- (void)setHidden:(BOOL)hidden
{
// Extra setter logic…
_hidden = hidden;
}
警告:从不从set<Key>:方法内部调用Validating Properties中描述的验证方法。
当一个属性是非对象类型(如布尔值hidden
)时,协议的默认实现会检测底层数据类型,并在将其应用于setter之前取消对来自setValue:forKey:
的对象值(本例中是NSNumber实例)的包装。你不需要在setter中处理这个问题。但是,如果有可能将nil值写入非对象属性,则可以重写setNilValueForKey:
来处理这种情况,如处理非对象值中所述。hidden
属性的适当行为可能只是将nil解释为NO:
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:@"hidden"]) {
[self setValue:@(NO) forKey:@"hidden"];
} else {
[super setNilValueForKey:key];
}
}
如果合适,即使允许编译器合成setter,也可以提供上述方法重写。
实例变量
当一个键值编码访问器方法的默认实现找不到属性的访问器时,它会直接查询其类的accessInstanceVariablesDirectly
方法,以查看该类是否允许直接使用实例变量。默认情况下,这个类方法返回YES,尽管您可以重写这个方法以返回NO。
如果您确实允许使用ivar
,请确保它们以通常的方式命名,使用以下划线_
为前缀的属性名。通常,编译器在自动合成属性时会为您执行此操作,但如果您使用显式的@synthey
指令,则可以自己强制执行此命名:
@synthesize title = _title;
在某些情况下,不使用@synthey
指令或允许编译器自动合成属性,而是使用@dynamic
指令通知编译器您将在运行时提供getter和setter。这样做可能是为了避免自动合成getter,以便可以提供集合访问器,如定义集合方法中所述。在这种情况下,您可以自己声明ivar
作为接口声明的一部分:
@interface MyObject : NSObject {
NSString* _title;
}
@property (nonatomic) NSString* title;
@end
3.2 定义集合方法
当使用标准命名约定创建访问器和ivar时,如实现基本键值编码遵从性中所述,键值编码协议的默认实现可以根据键值编码的消息来定位它们。对于表示许多关系的集合对象,这与对于其他属性一样正确。但是,如果实现集合访问器方法而不是集合属性的基本访问器,或者除此之外,还可以:
-
与NSArray或NSSet以外的类建立多个关系模型。在对象中实现集合方法时,键值getter的默认实现返回一个代理对象,该对象调用这些方法以响应其接收到的后续NSArray或NSSet消息。基础属性对象不必是NSArray或NSSet本身,因为代理对象使用集合方法提供预期的行为。
-
在对多对多关系的内容进行变异时获得更高的性能。该协议的默认实现使用您的集合方法对基础属性进行适当的变异,而不是使用基本setter重复创建新的集合对象以响应每次更改。
-
提供对对象集合属性内容的符合性访问的键值。有关键值观测的更多信息,请阅读《键值观测编程指南》。
您可以实现两种类型的集合访问器之一,具体取决于您是希望关系表现为索引的有序集合(如NSArray对象)还是无序的唯一集合(如NSSet对象)。在这两种情况下,您至少实现一组方法来支持对属性的读取访问,然后添加另一组方法来启用集合内容的变异。
注意:键值编码协议不声明本节中描述的方法。相反,NSObject提供的协议的默认实现会在与键值编码兼容的对象中查找这些方法,如访问器搜索模式中所述,并使用它们来处理作为协议一部分的键值编码消息。
访问索引集合
添加索引访问器方法以提供一种机制,用于计算、检索、添加和替换有序关系中的对象。底层对象通常是NSArray或NSMutableArray的实例,但如果提供集合访问器,则可以像操作数组一样操作实现这些方法的任何对象属性。
索引集合Getter
对于没有默认getter的集合属性,如果提供以下索引集合getter方法,则协议的默认实现将响应valueForKey:
消息返回一个行为类似于NSArray的代理对象,但调用以下集合方法来完成其工作。
注意:在现代Objective-C中,编译器在默认情况下为每个属性合成getter,因此默认实现不会创建使用本节中方法的只读代理(注意基本getter的搜索模式中的访问器搜索顺序)。您可以通过不声明属性(仅依赖于ivar)或将属性声明为@dynamic(表示您计划在运行时提供访问器行为)来解决此问题。不管怎样,编译器都不会提供默认getter,默认实现使用以下方法。
-
countOf<Key>
此方法将“多对多”关系中的对象数作为NSUInteger返回,就像NSArray原语方法count一样。实际上,当基础属性是NSArray时,可以使用该方法提供结果。
例如,对于表示银行交易列表并由称为交易的NSArray支持的多对多关系:
- (NSUInteger)countOfTransactions { return [self.transactions count]; }
-
objectIn<key>AtIndex: 或 <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]; }
索引集合变异器
支持与索引访问器的可变多对多关系需要实现一组不同的方法。当您提供这些setter方法时,响应mutableArrayValueForKey:
消息的默认实现将返回一个代理对象,该代理对象的行为类似于NSMutableArray对象,但使用对象的方法来完成其工作。这通常比直接返回NSMutableArray对象更有效。它还使多对多关系的内容与键值观测兼容(请参阅键值观测编程指南)。
为了使对象键值编码与可变的多对多关系兼容,请实现以下方法:
-
insertObject:in<Key>AtIndex: 或 insert<Key>:atIndexes:
第一个接收要插入的对象和指定要插入的索引的NSUInteger。第二个将对象数组插入到集合中由传递的NSIndexSet指定的索引处。这些方法类似于NSMutableArray方法
insertObject:atIndex:
和insertObjects:atIndex:
。只需要这些方法之一。- (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: 或 remove<Key>AtIndexes:
第一个接收指定要从关系中移除的对象的索引的NSUInteger值。第二个接收NSIndexSet对象,指定要从关系中删除的对象的索引。这些方法分别对应于NSMutableArray方法
removeObjectAtIndex:
和removeObjectsAtIndexes:
。只需要这些方法之一。- (void)removeObjectFromTransactionsAtIndex:(NSUInteger)index { [self.transactions removeObjectAtIndex:index]; } - (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes { [self.transactions removeObjectsAtIndexes:indexes]; }
-
replaceObjectIn<Key>AtIndex:withObject: 或 replace<Key>AtIndexes:with<Key>:
这些替换访问器为代理对象提供了一种直接替换集合中对象的方法,而无需依次移除一个对象并插入另一个对象。它们对应于NSMutableArray方法
replaceObjectAtIndex:withObject:
和replaceObjectsAtIndexes:withObjects:
。当分析应用程序时发现性能问题时,您可以选择提供这些方法。- (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]; }
访问无序集合
当您提供以下集合getter方法以返回集合中的对象数、遍历集合对象并测试集合中是否已存在对象时,协议的默认实现将响应valueForKey:
消息返回一个行为类似NSSet的代理对象,但调用以下收集方法来完成其工作。
注意:在现代Objective-C中,编译器在默认情况下为每个属性合成getter,因此默认实现不会创建使用本节中方法的只读代理(注意基本getter的搜索模式中的访问器搜索顺序)。您可以通过不声明属性(仅依赖于ivar)或将属性声明为@dynamic(表示您计划在运行时提供访问器行为)来解决此问题。不管怎样,编译器都不会提供默认getter,默认实现使用以下方法。
无序集合Getter
-
countOf<Key>
此必需方法返回关系中与NSSet方法计数相对应的项数。当底层对象是NSSet时,可以直接调用此方法。例如,对于名为employees的NSSet对象,包含Employee对象:
- (NSUInteger)countOfEmployees { return [self.employees count]; }
-
enumeratorOf<Key>
此必需的方法返回一个用于对关系中的项进行迭代的NSEnumerator实例。该方法对应于NSSet方法
objectEnumerator
。- (NSEnumerator *)enumeratorOfEmployees { return [self.employees objectEnumerator]; }
-
memberOf<Key>:
此方法将作为参数传递的对象与集合的内容进行比较,并返回匹配的对象作为结果,如果找不到匹配的对象,则返回nil。如果手动实现比较,则通常使用
isEqual:
比较对象。当基础对象是NSSet对象时,可以使用等效的member:
方法:- (Employee *)memberOfEmployees:(Employee *)anObject { return [self.employees member:anObject]; }
无序集合变异器
支持与无序访问器的可变多对多关系需要实现其他方法。实现可变无序访问器以允许对象提供无序的集代理对象以响应mutableSetValueForKey:
方法。实现这些访问器比依赖直接返回可变对象以更改关系中的数据的访问器要高效得多。它还使您的类键值观测与收集的对象兼容(请参阅键值观测编程指南)。
为了对可变无序到多个关系进行键值编码,请执行以下方法:
-
add<Key>Object: 或 add<Key>:
这些方法将单个项或一组项添加到关系中。向关系中添加一组项时,请确保关系中不存在等效对象。它们类似于NSMutableSet方法
addObject:
和unionSet:
。只需要这些方法之一。- (void)addEmployeesObject:(Employee *)anObject { [self.employees addObject:anObject]; } - (void)addEmployees:(NSSet *)manyObjects { [self.employees unionSet:manyObjects]; }
-
remove<Key>Object: 或 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]; }
3.3 处理非对象值
通常,与键值编码兼容的对象依赖于键值编码的默认实现来自动包装和展开非对象属性。但是,可以覆盖默认行为。这样做的最常见原因是处理在非对象属性上存储nil值的尝试。
注意:因为Swift中的所有属性都是对象,所以本节仅适用于Objective-C属性。
如果与键值编码兼容的对象接收到一条setValue:forKey:
消息,其中nil作为非对象属性的值传递,则默认实现没有适当的通用操作过程。因此,它会给自己发送一条setNilValueForKey:
消息,您可以覆盖它。
setNilValueForKey:
的默认实现:引发NSInvalidArgumentException异常,但您可以提供适当的特定于实现的行为。
例如,下面的代码通过将age设置为0来响应将一个人的年龄设置为nil值的尝试。
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:@"age"]) {
[self setValue:@(0) forKey:@”age”];
} else {
[super setNilValueForKey:key];
}
}
注意:为了向后兼容,当对象重写了deprecated的unableToSetNilForKey:方法时,setValue:forKey:调用该方法而不是setNilValueForKey:。
3.4添加验证
键值编码协议定义了通过键或键路径验证属性的方法。这些方法的默认实现反过来依赖于您按照与访问器方法相似的命名模式定义方法。具体地说,可以为任何具有要验证的名称键的属性提供validate<Key>:error:
方法。默认实现搜索此项以响应键编码的validateValue:forKey:error:
消息。
如果不为某个属性提供验证方法,则协议的默认实现假定该属性的验证成功,而不考虑该值。这意味着您可以选择逐个属性进行验证。
注意:您通常只使用Objective-C中描述的验证。在Swift中,属性验证更习惯于通过依赖编译器对选项和强类型检查的支持来处理,而使用内置的willSet和didSet属性观察程序来测试任何运行时API协定,如Swift编程语言(Swift 3)。
实现验证方法
当为属性提供验证方法时,该方法通过引用接收两个参数:要验证的值对象和用于返回错误信息的NSError。因此,验证方法可以采取以下三种操作之一:
-
值对象有效时,返回YES而不更改值对象或错误。
-
当值对象无效,并且您不能或不想提供有效的替代项时,请将error参数设置为一个NSError对象,该对象指示失败的原因并返回NO。
注意:在尝试设置错误引用之前,请始终测试该引用是否为空。
-
当值对象无效,但您知道有效的替代项时,创建有效对象,将值引用分配给新对象,并在不修改错误引用的情况下返回YES。如果提供另一个值,则始终返回一个新对象,而不是修改正在验证的对象,即使原始对象是可变的。
下面的代码演示了name
字符串属性的验证方法,该方法确保value
对象不是nil,并且名称是最小长度。如果验证失败,则此方法不替换其他值。
- (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;
}
标量值的验证
验证方法接收的参数是一个对象。因此,非对象属性的值包装在NSValue或NSNumber对象中,如在表示非对象值中所述。
下面的代码演示了标量属性age
的验证方法。在这种情况下,通过创建设置为零的有效值并返回YES来处理一个潜在的无效条件,即nil值。您还可以在setNilValueForKey:
覆盖中处理此特定条件,因为类的用户可能不会调用验证方法。
- (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;
}
3.5 描述属性关系
类描述提供了一种描述类中对单和对多属性的方法。定义类属性之间的这些关系允许使用键值编码对这些属性进行更智能、更灵活的操作。
类描述
NSClassDescription
是一个基类,它提供获取类的元数据的接口。类描述对象记录特定类的对象的可用属性以及该类的对象与其他对象之间的关系(一对一、一对多和多对一)。例如,attributeKeys
方法返回为类定义的所有属性的列表;toManyRelationshipKeys
和toOneRelationshipKeys
方法返回定义为多对一关系的键数组;和inverseRelationshipKey:
返回从所提供键的关系的目标指向接收方的关系的名称。
NSClassDescription
没有定义定义关系的方法。具体的子类必须定义这些方法。一旦创建,就可以使用NSClassDescriptionregisterClassDescription:forClass:
类方法注册类描述。
NSScriptClassDescription
是Cocoa中提供的NSClassDescription
的唯一具体子类。它封装了应用程序的脚本信息。
3.6 性能设计
键值编码是有效的,特别是当您依赖于默认实现来完成大部分工作时,但是它确实添加了一个间接级别,比直接方法调用稍微慢一些。只有当您可以从它提供的灵活性中获益,或者允许您的对象参与依赖于键值编码的Cocoa技术时,才使用键值编码。
重写键值编码方法
通常,通过确保对象继承自NSObject,然后提供本教程描述的特定于属性的访问器和相关方法,可以使对象的键值编码兼容。很少需要重写键值编码访问器的默认实现,如valueForKey:
和setValue:forKey:
,或者基于键的验证方法,如validateValue:forKey:
。因为这些实现缓存有关运行时环境的信息以提高效率,所以如果确实重写它们以引入自定义逻辑,请确保在返回之前调用超类中的默认实现。
优化对多个关系
当实现对多个关系时,访问器的索引形式在许多情况下都会提供显著的性能提升,特别是对于可变集合。
3.7 合规检查表
遵循本节总结的步骤,以确保您的对象符合键值编码。有关详细信息,请参见前几节。
属性和一种关系遵从性
对于作为属性或对一关系的每个属性:
-
实现一个名为
<key>
或is<key>
的方法,或者创建一个实例变量<key>
或_<key>
。编译器通常在自动合成属性时为您执行此操作。注意:尽管属性名称通常以小写字母开头,但协议的默认实现也适用于以大写字母开头的名称,例如URL。
-
如果属性是可变的,则实现set<Key>:方法。编译器通常在允许它自动合成属性时为您执行此操作。
重要:如果重写默认setter,请确保不要调用协议的任何验证方法。
-
如果该属性是标量,则重写
setNilValueForKey:
方法,以优雅地处理将nil值分配给标量属性的情况。
索引对多个关系遵从性
对于每个有序的多个关系(例如NSArray对象)的属性:
-
实现一个名为
<key>
的方法,该方法返回一个数组,或者有一个名为<key>
或_<key>
的数组实例变量。编译器通常在自动合成属性时为您执行此操作。 -
或者,实现方法
countOf<Key>
和objectIn<Key>AtIndex:
和<Key>AtIndex:
中的一个或两个。 -
(可选)实现
get<Key>:range:
以提高性能。
此外,如果属性是可变的:
-
实现
insertObject:in<Key>AtIndex:
和insert<Key>:AtIndex:
中的一个或两个方法。 -
实现
removeObjectFrom<Key>AtIndex:
和remove<Key>AtIndex:
方法中的一个或两个。 -
可选地,实现
replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndex:with<Key>:
以提高性能。
无序对多关系遵从性
对于无序的多对多关系(如NSSet对象)的每个属性:
-
实现返回集合的
<key>
,或具有名为<key>
或_<key>
的NSSet实例变量。编译器通常在自动合成属性时为您执行此操作。 -
或者,实现方法
countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>:
。
此外,如果属性是可变的:
-
实现一个或两个方法
add<Key>Object:
和add<Key>:
。 -
实现一个或两个方法
remove<Key>Object:
和remove<Key>:
。 -
可选地,实现
intersect<Key>:
以提高性能。
验证
选择对需要验证的属性进行验证:
- 实现
validate<Key>:error:
方法,返回指示值有效性的布尔值,以及适当时返回错误对象的引用。
网友评论