KVC
KVC的全称是Key-Value Coding(键值编码),是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问
,可以通过字符串来访问一个对象的成员变量或其关联的存取方法(getter
or setter
)
KVC还可以访问私有变量
KVC是许多其他 Cocoa 技术的基础概念,比如 KVO、Cocoa bindings、Core Data、AppleScript-ability 等等
访问对象属性
常用API
1 | - (nullable id)valueForKey:(NSString *)key; // 通过 key 来取值 |
基础操作
1 | @interface BankAccount : NSObject |
对于 BankAccount 的实例对象myAccount
KeyPath
KVC还支持多级访问,KeyPath用法跟点语法相同。 例如:我们想对myAccount的owner
属性的address
属性的street
属性赋值,其KeyPath为owner.address.street
1 | [myAccount setValue:@"地址" forKeyPath:@"owner.address.street"]; |
多值操作
给定一组Key,获得一组value,以字典的形式返回。该方法为数组中的每个Key调用valueForKey:
方法。
1 | - (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys; |
将指定字典中的值设置到消息接收者的属性中,使用字典的Key标识属性。默认实现是为每个键值对调用setValue:forKey:方法 ,会根据需要用nil替换NSNull对象
1 | - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues; |
访问集合属性
可以像访问其它对象一样使用valueForKey:
或setValue:forKey:
方法来获取或设置集合对象(主要指NSArray和NSSet),当我们要操作集合对象的内容,比如添加或者删除元素时,通过KVC的可变代理方法
获取集合代理对象是最有效的。
根据KVO的实现原理,是在运行时动态生成子类并重写setter方法来达到可以通知所有观察者对象的目的,因此我们对集合对象进行操作是不会触发KVO的
。当我们要使用KVO监听集合对象变化时,需要通过KVC的可变代理方法获取集合代理对象,然后对代理对象进行操作。当代理对象的内部对象发生改变时,会触发KVO的监听方法
。
三种不同的代理对象访问的代理方法,每种都有Key和KeyPath两种方法
mutableArrayValueForKey
: 和mutableArrayValueForKeyPath
: 返回NSMutableArray对象的代理对象。mutableSetValueForKey
: 和mutableSetValueForKeyPath
: 返回NSMutableSet对象的代理对象。mutableOrderedSetValueForKey
: 和mutableOrderedSetValueForKeyPath
: 返回NSMutableOrderedSet对象的代理对象
使用集合运算符
KVC的valueForKeyPath:方法除了可以取出属性值以外,还可以在KeyPath中嵌套集合运算符,来对集合对象进行操作。
以下是KeyPath集合运算符的格式,主要分为 3 个部分。
- Left key path:左键路径,要操作的集合对象,如果消息接收者就是集合对象,则可以省略 Left 部分;
- Collection operator:集合运算符;
- Right key path:右键路径,要进行运算的集合中的属性。
集合运算符主要分为三类:
① 聚合运算符:以某种方式合并集合中的对象,并返回右键路径中指定的属性的数据类型匹配的一个对象,一般返回NSNumber实例。
② 数组运算符:根据运算符的条件,将符合条件的对象以一个NSArray实例返回。
③ 嵌套运算符:处理集合对象中嵌套其他集合对象的情况,并根据运算符返回一个NSArray或NSSet实例。
聚合运算符
以某种方式合并集合中的对象,并返回右键路径中指定的属性的数据类型匹配的一个对象,一般返回NSNumber实例
@avg
读取集合中每个元素的右键路径指定的属性,将其转换为double类型 (nil用 0 替代),并计算这些值的算术平均值。然后将结果以NSNumber实例返回
1 | // 计算上表中 amount 的平均值。 |
@count
计算集合中的元素个数,以NSNumber实例返回。
1 | // 计算 transactions 集合中的元素个数。 |
@count运算符比较特别,它不需要写右键路径,即使写了也会被忽略。
@max,@min
@max和@min根据右键路径指定的属性在集合中搜索,搜索使用compare:方法进行比较,许多基础类 (如NSNumber类) 中都有定义。因此,右键路径指定的属性必须能响应compare:消息。搜索忽略值为nil的集合项。可以通过重写compare:方法对搜索过程进行控制。
数组运算符
根据运算符的条件,将符合条件的对象以一个NSArray实例返回。
@unionOfObjects
读取数组中每个元素的右键路径指定的属性,放在一个NSArray实例中并返回。
1 | // 获取集合中的所有 payee 对象。 |
@distinctUnionOfObjects
读取数组中每个元素的右键路径指定的属性,放在一个NSArray实例中,将数组进行去重后返回。
1 | // 获取集合中的所有不同的 payee 对象。 |
注意: 在使用数组运算符时,如果有任何操作的对象为nil,则valueForKeyPath:方法将引发异常。
嵌套运算符
处理集合对象中嵌套其他集合对象的情况,并根据运算符返回一个NSArray或NSSet实例
@unionOfArrays,@distinctUnionOfArrays,@distinctUnionOfSets
自定义集合运算符
使用Runtime打印NSArray类的方法列表
1 | - (void)printNSArrayMethods |
1 | 0---mr_isEqualToOutputDevicesArray: |
获取NSArray中的中位数
为NSArray添加一个分类,并定义一个_medianForKeyPath:方法,用来获取NSArray中的中位数
1 |
|
测试
1 | NSArray *array = @[@9, @7, @8, @2, @6, @3]; |
属性验证
可以在使用KVC赋值前验证能否为这个key赋值指定value。
validateValue
方法的默认实现是查看消息接收者类中是否实现了遵循命名规则为validate<Key>:error:
的方法,如果有的话就返回调用该方法的结果;如果没有的话,则默认验证成功并返回YES。我们可以在消息接收者类中实现validate<Key>:error:
的方法来自定义逻辑返回YES或NO
1 | - (BOOL)validateValue:(id _Nullable *)value |
默认情况下,KVC是不会自动验证属性的。
搜索规则
除了了解KVC的使用,了解KVC取值和赋值过程的工作原理也是很有必要的。
基本的 Getter 搜索模式
以下是valueForKey:方法的默认实现,给定一个key作为输入参数,在消息接收者类中操作,执行以下过程。
① 按照get
如果找到就调用取值并执行⑤,否则执行②;
② 查找countOf
如果找到第一个和后面两个中的至少一个,则创建一个能够响应所有NSArray的方法的集合代理对象(类型为NSKeyValueArray,继承自NSArray),并返回该对象。否则执行③;
代理对象随后将其接收到的任何NSArray消息转换为countOf
当KVC调用方与代理对象一起工作时,允许底层属性的行为如同NSArray一样,即使它不是NSArray。
③ 查找countOf
如果三个方法都找到,则创建一个能够响应所有NSSet的方法的集合代理对象(类型为NSKeyValueSet,继承自NSSet),并返回该对象。否则执行④;
代理对象随后将其接收到的任何NSSet消息转换为countOf
当KVC调用方与代理对象一起工作时,允许底层属性的行为如同NSSet一样,即使它不是NSSet。
④ 查看消息接收者类的+accessInstanceVariablesDirectly方法的返回值(默认返回YES)。如果返回YES,就按照_
⑤ 如果取到的值是一个对象指针,即获取的是对象,则直接将对象返回。
如果取到的值是一个NSNumber支持的数据类型,则将其存储在NSNumber实例并返回。
如果取到的值不是一个NSNumber支持的数据类型,则转换为NSValue对象, 然后返回。
⑥ 调用valueForUndefinedKey:方法,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定key做一些特殊处理。
基本的 Setter 搜索模式
以下是setValue:forKey:方法的默认实现,给定key和value作为输入参数,尝试将KVC调用方的属性名为key的值设置为value,执行以下过程。
① 按照set
如果找到就调用并将value传进去(根据需要进行数据类型转换),否则执行②。
② 查看消息接收者类的+accessInstanceVariablesDirectly方法的返回值(默认返回YES)。如果返回YES,就按照
③ 调用setValue:forUndefinedKey:方法,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定key做一些特殊处理。
NSMutableArray 搜索模式
以下是mutableArrayValueForKey:方法的默认实现,给定一个key作为输入参数,返回属性名为key的集合的代理对象(这里指NSMutableArray对象),在消息接收者类中操作,执行以下过程。
① 查找一对方法insertObject:in
(相当于NSMutableArray的原始方法insertObject:atIndex:和removeObjectAtIndex:),
或者insert
(相当于NSMutableArray的原始方法insertObjects:atIndexes:和removeObjectsAtIndexes:)。
如果我们至少实现了一个insertion方法和一个removal方法,则返回一个代理对象,来响应发送给NSMutableArray的消息,通过发送insertObject:in
该代理对象类型为NSKeyValueFastMutableArray2,继承链为NSKeyValueFastMutableArray2->NSKeyValueFastMutableArray->NSKeyValueMutableArray->NSMutableArray。
如果我们也实现了一个可选的replace object方法,如replaceObjectIn
② 查找set
如果找到,就会向KVC调用方发送一个set
该代理对象类型为NSKeyValueSlowMutableArray,继承链为NSKeyValueSlowMutableArray->NSKeyValueMutableArray->NSMutableArray。
注意:
此步骤中描述的机制比上一步的效率低得多,因为它可能重复创建新的集合对象,而不是修改现有的集合对象。因此,在设计自己的键值编码兼容对象时,通常应该避免使用它。
给代理对象发送NSMutableArray消息都会调用set
③ 查看消息接收者类的+accessInstanceVariablesDirectly方法的返回值(默认返回YES)。如果返回YES,就按照_
④ 返回一个可变的集合代理对象。当它接收到NSMutableArray消息时,发送一个valueForUndefinedKey:消息给KVC调用方,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定key做一些特殊处理。
异常处理
① 根据KVC搜索规则,当没有搜索到对应的key或者keyPath相关方法或者变量时,会调用对应的异常方法valueForUndefinedKey
:或setValue:forUndefinedKey:
,这两个方法的默认实现是抛出异常NSUnknownKeyException,并导致程序Crash。我们可以重写这两个方法来处理异常
1 | - (nullable id)valueForUndefinedKey:(NSString *)key; |
② 当进行赋值如setValue:forKey:时,如果key的数据类型是非对象类型,则value就禁止传nil。否则会调用setNilValueForKey:
方法,该方法的默认实现是抛出异常NSInvalidArgumentException
,并导致程序Crash。我们可以重写这个方法来处理异常。
1 | - (void)setNilValueForKey:(NSString *)key |