Key-Value Coding Programming Guide
Key-value coding是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用该协议来提供对其属性的间接访问。当对象符合KVC时,可以通过简洁、统一的消息传递接口通过字符串参数对其属性进行寻址。这种间接访问机制补充了实例变量及其关联访问器方法提供的直接访问。
通常使用getter方法来访问对象的属性。getter返回属性的值。setter设置属性的值。在Objective-C中,还可以直接访问属性的底层实例变量。遵循KVC的对象提供了一个简单的消息传递接口,该接口与其所有属性保持一致。
KVC是许多其他Cocoa技术的基础概念,例如key-value observing、Cocoa bindings、Core Data和AppleScript-ability。在某些情况下,还可以帮助简化代码。
使用符合 Key-Value Coding 的对象
对象从NSObject(直接或间接)继承时通常采用KVC,NSObject既采用NSKeyValueCoding协议,又为基本方法提供了默认实现。这样的对象允许其他对象通过一个紧凑的消息传递接口来完成以下操作:
访问对象的属性。协议指定方法,例如通用getter
valueForKey:
和通用settersetValue:forKey:
,用于通过对象的名称或键(参数化为字符串)访问对象属性。这些相关方法的默认实现使用键来定位底层数据并与之交互, 如Accessing Object Properties。操作集合属性。操作集合属性的访问方法的默认实现与对象的集合属性(如NSArray对象)一起工作,就像任何其他属性一样。此外,如果对象为属性定义了集合访问器方法,则它支持对集合内容的key-value访问。这通常比直接访问更有效,并允许通过标准化接口使用自定义集合对象,如 Accessing Collection Properties。
在集合对象上调用集合操作符。当访问符合KVC的对象中的集合属性时,可以向key字符串中插入集合操作符,如Using Collection Operators。集合操作符指示默认的NSKeyValueCoding
getter
实现对集合进行操作,然后返回一个新的、过滤过的集合版本,或者返回一个表示集合某些特征的单个值。访问非属性。协议的默认实现检测非对象属性,包括标量和结构,并自动将它们包装和展开为对象,以便在协议接口上使用,如在Representing Non-Object Values。此外,该协议声明了一个方法,允许兼容的对象在通过KVC接口在非对象属性上设置nil值时提供合适的操作。
通过key path访问属性。当有一个遵循KVC的层次结构对象时,可以使用key path的方法调用来向下挖掘,使用单个调用在层次结构内部获取或设置一个深层次的值。
对对象使用 Key-Value Coding
为了使自己的对象KVC兼容,要确保采用NSKeyValueCoding
非正式协议并实现相应的方法,如作为通用getter的valueForKey:
和作为通用setter的setValue:forKey:
。NSObject采用了这个协议,并为这些方法和其他基本方法提供了默认实现。因此,如果从NSObject派生对象(或其众多子类中的任何一个),那么以上大部分工作已经完成了。
为了让默认方法执行它们的工作,要确保对象的访问器方法和实例变量遵守某些定义良好的模式。这允许默认实现在响应KVC的消息时找到对象的属性。然后,通过提供验证和处理某些特殊情况的方法,可以选择扩展和定制。
其他依赖于KVC的Cocoa技术
符合的对象可以应用于各种依赖于这种访问的Cocoa技术,包括:
Key-value observing。键-值观察该机制允许对象注册由另一个对象的属性更改驱动的异步通知,如Key-Value Observing Programming Guide中所述。
Cocoa bindings。 这些技术完全实现了Model-View-Controller 范例,其中models封装应用程序数据,views显示和编辑数据,controllers在两者之间进行协调。了解更多关于Cocoa绑定的信息参阅 *Cocoa Bindings Programming Topics。
Core Data。 该框架为与对象生命周期和对象图管理(包括持久性)相关的常见任务提供了通用的和自动化的解决方案。
访问对象属性
对象通常在其@interface声明中指定属性,这些属性有以下几种类别:
Attributes。属性,这些是简单的值,例如scalars、strings或Boolean值。值对象(如NSNumber)和其他不可变类型(如NSColor)也被视为属性。
To-one relationships一对一的关系。这些是可变的对象,具有自己的属性。对象的属性可以在对象本身不发生变化的情况下更改。例如,银行帐户对象可能有一个owner属性,该属性是Person对象的实例,而Person对象本身具有address属性。owner的address可能会改变,而银行帐户的所有者owner没有改变。改变的只有所有者的地址。
To-many relationships 一对很多关系。这些是集合对象。通常使用NSArray或NSSet的实例来保存这样的集合,也可以使用自定义集合类。
清单2-1中声明的BankAccount对象演示了每种类型的属性。
@interface LMBack : NSObject
@property(nonatomic)NSNumber *currentBalance;
@property(nonatomic)LMPerson *person;
@property(nonatomic)NSArray *records;
@end
为了保持封装,对象通常为属性提供访问器方法。可以显式地编写这些方法,也可以依赖编译器自动合成它们。不管怎样,使用这些访问器之一的代码的作者必须在编译代码之前将属性名写入代码。访问器方法的名称将成为使用它的代码的静态部分。例如,给定清单2-1中声明的bank account对象,编译器将合成一个setter,可以为myAccount实例调用它:
[myAccount setCurrentBalance:@(100.0)];
这是直接调用,但缺乏灵活性。与key-value coding 兼容的对象提供了一种更通用的机制,可以使用字符串标识符访问对象的属性。
用 Keys 和 Key Paths 标识对象的属性
键是标识特定属性的字符串。通常,表示属性的键是在代码中显示的属性本身的名称。键必须使用ASCII编码,不能包含空格,并且通常以小写字母开头(尽管也有例外,如在许多类中的URL属性)。
因为清单2-1中的BankAccount类是KVC兼容的,所以它识别键owner、currentBalance和transactions,它们是其属性的名称。可以通过它的键设置值:
[myAccount setValue:@(100.0)forKey:@“currentBalance”];
可以使用相同的方法,使用不同的键来设置myAccount对象的所有属性。因为参数是字符串,所以它可以在run-time时操作的变量。
key-path是一系列点分隔键,用于指定要遍历的对象属性序列。序列中第一个键的属性是相对于接收器的,每个后续键都是相对于前一个属性的值来计算的。key-path对于通过单个方法调用深入到对象层次结构非常有用。
例如,owner.address.street
应用于银行帐户实例是指存储在银行帐户所有者owner地址address中的街道street字符串的值,假设Person和address类也符合键值编码。
[myAccount valueForKeyPath:@"owner.address.street"]);
注意:在Swift中,您可以使用#keyPath表达式,而不是使用字符串来指示键或键路径。这提供了编译时检查的优势,如使用Swift with Cocoa和Objective-C(Swift 3)指南中的Keys and Key Paths部分所述。
使用 Keys 获取属性值
当对象采用NSKeyValueCoding协议时,它是NSKeyValueCoding
协议的。从NSObject继承的对象提供了协议基本方法的默认实现,它会自动采用具有某些默认行为的协议。这样的对象至少实现了以下基本的基于键的getter:
valueForKey:-返回由key参数命名的属性的值。如果根据Accessor Search Patterns中描述的规则,无法找到由键命名的属性,则对象将向自己发送valueForUndefinedKey:信息。
valueForUndefinedKey:
的默认实现引发了一个NSUndefinedKeyException
,但子类可能会覆盖此行为并更优雅地处理这种情况。valueForKeyPath:-返回指定key-path对于接收器的值。key-path序列中的任何对象,如果不符合特定键的键值编码,
valueForKey:
的默认实现找不到访问器方法,则会收到valueForUndefinedKey:
消息。dictionaryWithValuesForKeys:—返回与接收器相关的键数组的值。该方法为数组中的每个键调用
valueForKey:
。返回的NSDictionary包含数组中所有键的值。
注意:集合对象(如NSArray、NSSet和NSDictionary)不能包含nil值。使用NSNull对象表示零值。NSNull提供一个表示对象属性的nil值的实例。dictionaryWithValuesForKeys:
和相关的setValuesForKeysWithDictionary:
的默认实现在NSNull(在dictionary参数中)和nil(在存储的属性中)之间自动转换。
使用键路径寻址属性时,如果键路径中的最后一个键是对多关系(即,它引用集合),则返回的值是一个集合,其中包含对多键右侧键的所有值。例如,请求key path的records.balance值返回一个数组,该数组包含所有事务的所有款项金额。这也适用于key path中的多个数组。key pathaccounts.records.payee
返回一个包含所有帐户中所有交易的所有受款人对象的数组。
for (NSInteger i = 0; i < 3; i++) {
Transaction *t = [[Transaction alloc] init];
LMPerson *p = [[LMPerson alloc] init];
p.name = [NSString stringWithFormat:@"%@-啊哈哈", @(i)];
t.person = p;
t.balance = @(i*100);
[arr addObject:t];
}
back.records = [arr copy];
NSLog(@"%@", [back valueForKeyPath:@"records.balance"]);
2020-08-04 17:23:42.629599+0800 Test[57233:8661545] (
0,
100,
200
)
使用 Key 设置属性值
与getter一样,基于NSObject中找到的NSKeyValueCoding协议的实现,符合KVC的对象也提供了一组通用setter:
setValue:forKey::-将相对于接收消息的对象的指定键的值设置为给定值。
setValue:forKey:
的默认展开表示标量和结构的NSNumber和NSValue对象,并将它们指定给属性。有关包装和展开语义的详细信息,请参见 Representing Non-Object Values 。
如果指定的键对应于接收setter调用的对象不具有的属性,则该对象向自身发送一个setValue:forUndefinedKey:消息。setValue:forUndefinedKey:
引发NSUndefinedKeyException
。但是,子类可以重写此方法以自定义方式处理请求。setValue:forKeyPath:—设置相对于接收器的指定key path路径处的给定值。在一个特定的密钥序列中,任何一个key path序列中的key-value 都是不兼容的
setValue:forUndefinedKey:
消息。setValuesForKeysWithDictionary:-使用指定字典中的值设置接收器的属性,使用字典键标识属性。默认实现对每个key-value pair调用
setValue:forKey:
,根据需要用nil
替换NSNull
对象。
在默认实现中,当尝试将一个非对象属性设置为nil值时,遵循key-value coding 的对象向自己发送一个setNilValueForKey:消息。setNilValueForKey:
会引发一个NSInvalidArgumentException
异常,但可以重写这个行为来代替默认值或标记值,如处理Handling Non-Object Values。
Using Keys to Simplify Object Access --使用键简化对象访问
要了解基于键的getter和setter如何简化代码,请参考以下示例。在macOS中,NSTableView
和NSOutlineView
对象将一个标识符字符串与它们的每一列关联起来。如果支持表的模型对象不符合KVC,表的数据源方法将被迫依次检查每个列标识符,以找到要返回的正确属性,如清单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;
}
利用key-value coding 兼容的' Person '对象,以列标识符作为key,使用valueForKey:
的getter数据源方法返回适当的值。在以后添加新列时,只要列标识符始终与模型对象的属性名称匹配,就会继续工作。
- (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)。
访问器搜索模式
NSObject提供的NSKeyValueCoding
协议的默认实现使用一组明确定义的规则将key-based accessor的访问器调用映射到对象的底层属性。这些协议方法使用一个key参数来搜索它们自己的对象实例,以查找访问器accessors、实例变量和遵循特定命名约定的相关方法。理解这种默认搜索是如何工作的还是很有帮助的,这有助于跟踪key-value编码的对象的行为,也有助于使您自己的对象兼容。
请注意 :本节中的描述使用<key>或作为key-value coding协议方法中作为参数出现的<Key>字符串的占位符,然后该方法将其用作辅助方法调用或变量名称查找的一部分。映射的属性名称遵循占位符的大小写。例如,对于getters <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:
的一些组合,这些组合将消息发送给创建它的KVC对象。如果原始对象还实现了一个名为get<Key>:range:
的可选方法,代理对象也会在适当的时候使用它。实际上,代理对象与和KVC兼容的对象一起工作,允许底层属性像NSArray
一样工作,即使它不是NSArray
。如果没有找到简单的访问方法或数组访问方法组,查找三个方法,分别为
countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>:
(对应于NSSet
类定义的原语方法)。
如果这三个方法都找到了,创建一个集合代理对象,它响应所有NSSet
方法并返回那个。否则,继续执行步骤4。
这个代理对象随后将它接收到的所有NSSet
消息转换为countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>:
消息的组合,并发送给创建它的对象。实际上,代理对象与遵循KVC的对象一起工作,允许底层属性像NSSet
一样运行,即使它不是NSSet
。如果没有找到简单的访问方法或集合访问方法组,并且如果接收方的类方法accessInstanceVariablesDirectly直接返回
YES
,那么按照顺序搜索一个实例变量:_<key>
,_is<Key>
,<key>
, 或is<Key>
, 。如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤6。如果检索到的属性值是一个对象指针,只需返回结果。
如果值是NSNumber
支持的类型,将其存储在NSNumber
实例中并返回。
如果结果是NSNumber不支持的类型,转换为NSValue
对象并返回它。如果所有其他方法都失败,则调用valueForUndefinedKey:。这在默认情况下会引发一个NSUndefinedKeyException异常,但是
NSObject
的一个子类可能提供key-specific
的行为。
Setter的搜索模式
setValue:forKey:
的默认实现,给定 key
和value
参数作为输入,尝试在接收调用的对象内部设置一个名为key
的属性为value
使用以下过程:
按照顺序查找第一个访问器
set<Key>:
或_set<Key>
。如果找到,使用输入值(或根据需要打开包装值)完成赋值。如果没有找到简单的访问器,并且类方法(是否可以访问成员变量)
accessInstanceVariablesDirectly
返回YES
,那么用如下名称_<key>
,_is<Key>
,<key>
, oris<Key>
查找一个实例变量。如果找到,直接用输入值(或打开包装的值)和完成设置变量。在没有找到访问器或实例变量时,调用
setValue:forUndefinedKey:
,默认情况下会引发一个异常,但是NSObject
的子类可能提供key-specific的行为。
搜索可变数组的模式
mutableArrayValueForKey:的默认实现,给定一个key
参数作为输入,在接收访问器调用的对象内部返回一个名为key
的属性的可变代理数组,使用以下过程:
- 查找一对方法名字像
insertObject:in<Key>AtIndex:
和removeObjectFrom<Key>AtIndex:
(对应NSMutableArray原始insertObject:atIndex:
和removeObjectAtIndex:
),或方法名称如insert<Key>:atIndexes:
和remove<Key>AtIndexes:
(NSMutableArray的insertObjects:atIndexes:
和removeObjectsAtIndexes:
方法)。
如果对象至少有一个插入法和至少一个移除方法,返回一个代理对象,该对象通过发送insertObject:in<Key>AtIndex:
,removeObjectFrom<Key>AtIndex:
,insert<Key>:atIndexes:
,和remove<Key>AtIndexes:
的消息组合给原始的mutableArrayValueForKey:
接收者来响应NSMutableArray
消息。
当对象接收到mutableArrayValueForKey:
消息时,也会实现一个可选的替换对象方法,该方法的名称类似于replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
,代理对象也会在适当的时候使用这些方法,以获得最佳性能。 - 如果对象没有可变数组方法,则查找名称与模式集:匹配的访问器方法。在这种情况下,返回一个代理对象响应NSMutableArray消息,通过发出一个set:消息给原始接收mutableArrayValueForKey:。
注意:此步骤中描述的机制的效率远低于前一步,因为它可能涉及重复创建新的集合对象,而不是修改现有的集合对象。因此,在设计自己的键-值编码兼容的对象时,通常应该避免使用它。
如果既没有找到可变数组方法,也没有找到访问器,并且如果接收方的类对accessInstanceVariablesDirectly直接响应
YES
,那么按照顺序搜索一个具有类似于_<key>
或<key>
的实例变量。
如果找到了这样的实例变量,则返回一个代理对象,该代理对象将它接收到的每个NSMutableArray
消息转发给实例变量的值,该值通常是NSMutableArray
的一个实例或它的一个子类。如果所有这些都失败,返回一个可变的集合代理对象,每当它收到
NSMutableArray
消息时,该对象向mutableArrayValueForKey:
消息的原始接收方发出setValue:forUndefinedKey:
消息。
setValue:forUndefinedKey:
的默认实现会引发NSUndefinedKeyException
异常,但子类可能会覆盖此行为。
KVC是否有违面向对象编程思想(破坏面向对象方法)?
KVC的主要方法 valueForKey: 和 setValue: forKey:, 对 key 是没有限制的,在知道一个类内部的私有成员变量的名称情况下,在外部可以通过key访问和设置这个内部的私有成员变量,从这个角度来讲,KVC是有OOP思想的。
KVC的赋值和取值过程是怎样的?原理是什么?
- KVC 通过
setValue: forKey:
或setValue: forKeyPath:
赋值
- KVC 在修改值得前后会分别自动调用willChangeValueForKey和didChangeValueForKey:
-
+(BOOL)accessInstanceVariablesDirectly是否可以访问成员变量默认是 YES, 可以访问,所当根据 key找到的是成员变量时(没有setKey:和 _setKey:方法)也会触发 KVO
-
-
KVC 通过 valueForKey:取值