KVC基本概念
Key-Value Coding
(简称KVC
)是由 NSKeyValueCoding
非正式协议启用的一种机制,对象采用这种机制来提供对 其属性的间接访问。
当对象符合Key-Value Coding
标准时,可以通过简洁、统一的 消息传递接口 通过 字符串参数 访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。
以下代码是我们经常使用KVC
进行的操作:
1 | @interface Car : NSObject |
1 | ZMCar *c = [[ZMCar alloc] init]; |
KVC的作用
通常,我们使用访问器方法访问对象的属性,如:getter
方法 和 setter
方法。在Objective-C
中,也可以直接访问属性的底层实例变量。这些方式访问对象的属性都很简单,但需要调用特定于属性的方法或变量名。随着属性列表的增加或更改,访问这些属性的代码也必须随之增加,缺乏灵活性。相比之下,遵循 Key-Value Coding
的对象提供了一个简单的消息传递接口,该接口在其所有属性之间都是一致的。
Key-Value Coding
是其他许多Cocoa
技术的基础概念,如:Key-value-observation(KVO)、Cocoa bindings、Core Data和 AppleScript-ability。在某些情况下,KVC
还可以帮助简化代码。
KVC可以执行的操作
NSObject
采用了 NSKeyValueCoding
协议,并为基本方法提供了默认实现。这样继承自NSObject
的对象使其他对象能够通过紧凑的消息传递接口执行以下操作:
访问对象的属性
操作集合属性
在集合对象上调用集合运算符
访问非对象属性
通过
Key-Path
访问属性
KVC访问对象的属性
用Key
和 key path
标识对象的属性
键(Key
)是标识特定属性的字符串。通常,按照约定,表示属性的键是在代码中显示的属性本身的名称。
键路径(key path
)是一系列点分隔的键(Key
),用于指定要遍历的对象(遵循KVC
的层次结构对象)属性序列。
1 | - (void)setValue:(nullable id)value forKey:(NSString *)key; |
使用 Key 设置属性值
setValue:forKey:
将指定Key
相对于接收消息的对象的值设置为给定值。
如果指定Key
的对应于接收setter
调用的对象所不具有的属性,则该对象将向自身发送setValue:forUndefinedKey:
消息。setValue:forUndefinedKey:
的默认实现引发NSUndefinedKeyException
异常,子类可以重写此方法以自定义方式处理请求。setValue:forKeyPath:
在相对于接收方指定Key Path
处的设置给定值。Key Path
序列中不符合特定Key
的键值编码的任何对象都会收到setValue:forUndefinedKey:
消息。setValuesForKeysWithDictionary:
使用字典Key
标识属性,使用指定字典中的Value
设置接收方的属性。
默认实现为字典的每个 键-值对 调用setValue:forKey:
,并根据需要用nil
替换NSNull
对象。
在默认实现中,当您尝试将一个非对象属性设置为nil
值时,KVC
对象会向自己发送一个setNilValueForKey:
消息。setNilValueForKey:
的默认实现会引发一个NSInvalidArgumentException
异常,但对象可以重写这个方法来代替默认值或标记值,如处理非对象值中所描述的那样。
使用 Key 获取属性的值
valueForKey:
返回由key
参数命名的属性的值。
如果无法找到由Key
命名的属性,则KVC
对象将向自己发送valueForUndefinedKey:
消息,valueForUndefinedKey:
的默认实现引发了一个NSUndefinedKeyException
,子类通常重写此方法并更优雅的处理这种情况。valueForKeyPath:
返回相对于接收者的指定Key Path
的值。
在Key Path
序列中任何不符合KVC
的对象,也就是说,valueForKey:
的默认实现无法找到访问器方法,该对象均会接收到valueForUndefinedKey:
消息。dictionaryWithValuesForKeys:
返回与接收器相关的键数组的值。
该方法为数组中的每个Key
调用valueForKey:
。 返回的NSDictionary
包含数组中所有键的值。
如下例子,访问对象的属性:
1 | @interface ZMPart : NSObject |
1 | [tool setValue:@2 forKey:@"objCount"]; |
KVC操作集合属性
符合 KVC
的对象,可以通过 其协议提供的方法来操作集合类型的属性内容。协议提供了如下方法:
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
返回行为类似于NSMutableArray
对象的代理对象。mutableSetValueForKey:
和mutableSetValueForKeyPath:
返回行为类似于NSMutableSet
对象的代理对象。mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
返回行为类似于NSMutableOrderedSet
对象的代理对象。
通过如下例子来理解代理对象:
1 | [tool setValue:@[@1, @2, @3] forKey:@"days"]; |
1 | [tool setValue:@[@"北京", @"河北"] forKeyPath:@"part.place"]; |
通过对代理对象进行操作集合的内容,协议的默认实现将相应地修改底层属性。这比valueForKey:
获取不可变的集合对象,然后创建一个内容已修改的对象,最后使用setValue:forKey:
存回更有效。
这些方法的另一个好处是可以维护集合对象中保存的对象的KVO
遵从性。
使用集合运算符
当向符合KVC
的对象发送 valueForKeyPath:
消息时,可以在 Key-Path
中嵌入 集合运算符。
集合运算符(Collection Operators
)是前面带有一个@符号
一小串关键字中的一个,它指定了getter
在返回数据之前应该执行的操作,以某种方式对数据进行操作。NSObject
默认实现实现了此行为。
当Key-Path
包含集合运算符时,使用格式如下:
Left Key Path
:称为左键路径,它指集合运算符之前的Key-Path
的任何部分,指示相对于消息接收方要操作的集合。如果将消息直接发送给一个集合对象,比如NSArray
实例,则可能会省略左键路径。Right Key Path
:称为右键路径,它指集合运算符后面的Key-Path
部分,指定了该运算符应处理的集合中的属性。除@count
之外的所有集合运算符都需要一个正确的Key-Path
。
Collection Operators
的三种基本行为类型:
Aggregation Operators
(聚合运算符),以某种方式合并集合对象,并返回一个通常与右键路径中指定的属性的数据类型匹配的单个对象。@count
运算符符是个例外,它不接受右键路径,并且总是返回一个NSNumber
实例。Array Operators
(数组运算符),返回一个NSArray实例,该实例包含命名集合中包含的某些对象子集。Nesting Operators
(嵌套运算符),在包含其他集合的集合上工作,并根据运算符返回NSArray
或NSSet
实例,该实例以某种方式组合嵌套集合的对象。
下面例子均使用以下对象进行:
1 | @interface Person : NSObject |
Aggregation Operators
聚合运算符 用于处理数组或属性集,生成反映集合某些方面的单个值。
@avg
运算符,valueForKeyPath:
读取集合中每个元素的右键路径指定的属性,将其转换为双精度值(用0替换nil值),并计算这些值的算术平均值。然后返回存储在NSNumber
实例中的结果。@count
运算符,valueForKeyPath:
返回NSNumber
实例中集合中的对象数量。如果存在right key path
将被忽略。@max
运算符,valueForKeyPath:
在由右键路径命名的集合项中搜索并返回最大的项。搜索使用Foundation
中的compare:
方法进行比较,因此,右键路径所指示的属性必须包含对该方法有意义响应的对象。@min
运算符,与@max
运算符相反。@sum
运算符,valueForKeyPath:
读取由右键路径为集合的每个元素指定的属性,将其转换为double
(用0代替nil
值),并计算这些值的总和。将结果返回存储在NSNumber
实例中。
基本使用如下例子:
1 | self.peoples = @[[Person personWithDict:@{@"name":@"张三", @"age":@18, @"birthday":@"2001-02-22"}], |
Array Operators
数组运算符 使valueForKeyPath:
返回一个对象数组,该对象数组与右键路径指示的一组特定对象相对应。
如果使用数组运算符时有任何子对象为
nil
,则valueForKeyPath:
方法将引发异常。
@distinctUnionOfObjects
运算符,valueForKeyPath:
创建并返回一个数组,该数组包含集合中与由右键路径指定的属性相对应的不重复对象。@unionOfObjects
运算符,与@distinctUnionOfObjects
类似,但是返回的数组没有删除重复的对象。
1 | NSArray *distinct = [self.peoples valueForKeyPath:@"@distinctUnionOfObjects.name"]; |
Nesting Operators
嵌套运算符 对嵌套的集合进行操作,其中集合本身的每个条目都包含一个集合。
使用嵌套运算符时,如果任何叶子对象为
nil
,则valueForKeyPath:
方法将引发异常。
@distinctUnionOfArrays
运算符,valueForKeyPath:
创建并返回一个数组,该数组包含与右键路径指定的属性相对应的所有集合的组合的不重复的对象。@unionOfArrays
运算符,与@distinctUnionOfArrays
运算符类似,但不会删除重复的对象。@distinctUnionOfSets
运算符,valueForKeyPath:
创建并返回一个NSSet
对象,该对象包含与右键路径指定的属性相对应的所有集合的组合的不重复的对象。但是它要求操作的是的NSSet
类型的实例。
1 | self.boy = @[[Person personWithDict:@{@"name":@"张三", @"age":@18, @"birthday":@"2001-02-22"}], |
1 | NSArray *somePeople = @[self.boy, self.girl]; |
访问非对象属性
NSObject
遵守KVC
协议的方法默认实现可以同时处理对象(object
)和非对象(non-object
)属性。默认实现在 对象参数 或 返回值 与 非对象属性 之间自动转换。
non-object properties
分为两类,一类是基本数据类型(int、float、Bool...
)也就是所谓的标量(scalar
),一类是结构体(struct
)。
当使用
KVC
协议中的一个取值方法(如:valueForKey:
)时,如果返回值不是一个对象,getter
使用这个值来初始化一个NSNumber
对象(用于标量)或NSValue
对象(用于结构体),然后返回该值。在使用
setValue:forKey:
之类的设置器,给定特定键的情况下,如果数据类型不是对象,那么setter
首先向传入的Value
对象发送适当的<type>Value
消息,以提取底层数据,并将其存储起来。对非对象属性使用
nil
值调用一个KVC
协议设置器时,它会向接收setter
调用的对象发送一个setNilValueForKey:
消息。这个方法的默认实现会引发一个NSInvalidArgumentException
异常,一般子类可能会覆盖这个行为。
下面例子演示了利用KVC
对non-object properties
(非对象属性)的访问:
1 | typedef struct { |
使用NSNumber
实例包装的标量类型,NSValue
封装结构体类型。
1 | [scalar setValue:[NSNumber numberWithFloat:0.25] forKey:@"num"]; |
通过KVC
把非对象属性设置为nil
,会向KVC
对象发送setNilValueForKey:
消息,并抛出异常:
1 | [scalar setValue:nil forKey:@"count"]; |
利用KVC
访问自定义struct
类型的属性:
1 | ThreePoint three = {1.2, 2.2, 2.3}; |
验证属性
KVC
的协议中定义了支持属性验证的方法,即对属性值的合法性进行校验,通过如下两个方法实现的:
1 | - (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue |
当你调用上面的验证方法时,KVO
的默认实现将在接收验证消息的对象(或keyPath
中最后的对象)中搜索一种方法,其方法名与validate<Key>:error:
模式匹配。
- 如果对象没有这样的方法,则默认验证成功,返回
YES
。 - 当特定于属性的验证方法(
validate<Key>:error:
)存在时,默认实现将返回调用该方法的结果。
由于validate<Key>:error:
方法通过引用接收值(value
)和错误参数(error
),因此验证有三种可能的结果:
- 验证方法认为
value
有效,并返回YES
,而不改变value
或error
。 - 验证方法认为
value
无效,但并不修改它,返回NO
,并将error
设置为NSError
对象。 - 验证方法认为
value
无效,创建一个新的有效对象替换value
。方法返回YES
,同时保持error对象不变。
如下面例子:
1 | @interface MyValidate : NSObject |
1 | MyValidate *obj = [[MyValidate alloc] init]; |
通常,
KVC
的默认实现都没有定义任何自动执行验证的机制,一般我们在合适的地方提供验证方法。通常,仅在
Objective-C
中使用此处描述的验证在Swift
中,属性验证更习惯地通过依赖编译器对Optional
和 强类型 检查的支持来处理,而使用willSet
和didSet
来测试运行时API的约定。
访问搜索模式
NSObject
提供的NSKeyValueCoding
协议的默认实现,使用一组明确定义的规则将基于键(Key
)的访问器调用映射到对象的底层属性。这些协议方法使用一个键(Key
)参数在自己的对象实例中搜索访问器、实例变量 和 遵循特定命名约定的相关方法。
尽管您很少修改这种默认搜索,但是理解它是如何工作的还是很有帮助的,这有助于跟踪KVC
对象的行为,也有助于使你自己的对象兼容。
通过KVC
间接访问对象的属性,其取值(getter
)和赋值(setter
)的工作过程如下:
基本的 Getter 搜索模式
执行valueForKey:
时,在该方法调用类实例中默认进行以下操作:
1、 按顺序在实例中搜索第一个名为get<Key>
、<Key>
、is<Key>
或_<Key>
的访问器方法。如果找到了,就调用它,并继续执行步骤5。否则执行步骤2。
2、 如果找不到简单的访问器方法,则在实例中搜索countOf<Key>
和objectIn<Key>AtIndex:
(对应于NSArray
定义的基本方法)和<Key>AtIndexes:
(对应于NSArray
方法objectsAtIndexes:
)匹配的方法。
如果找到其中第一个(
countOf<Key>
),以及后面两个方法中的至少一个,则创建一个响应所有NSArray
方法的集合代理对象,并返回该对象。否则,继续执行步骤3。该代理对象会将它接收到的任何
NSArray
消息转换为countOf<Key>
、objectIn<Key>AtIndex:
和<Key>AtIndex:
消息的某种组合,组合将消息发送给创建它的KVC
对象。如果原始对象还实现了get<Key>:range:
方法,那么代理对象也将在适当的时候使用该方法。代理对象和KVC
对象一起使用,使底层属性的行为像NSArray
一样工作,即使它不是。
3、 如果找不到简单的访问器方法和数组访问方法,则查找countOf<Key>
、enumeratorOf<Key>
和 memberOf<Key>:
(对应于NSSet
类定义的基本方法)。
- 如果这三个方法都找到了,则创建一个响应所有
NSSet
方法的集合代理对象,并返回该对象。否则,执行步骤4。 - 该代理对象随后将接收到的所有
NSSet
消息转换为countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>:
消息的某种组合,并发送给创建它的对象。代理对象与KVC
对象一起工作,使底层属性像NSSet
一样运行,即使它不是NSSet
。
4、如果没有找到简单的访问方法或集合访问方法组,并且接收者的类方法accessInstanceVariablesDirectly
返回YES
。则按顺序搜索名为_<key>
、_is<Key>
、<key>
、is<Key>
的实例变量。
- 如果找到,直接获取实例变量的值,然后执行步骤5;否则执行步骤6。
5、如果检索到的属性值是一个对象指针,只需返回结果。
如果值是NSNumber
支持的标量类型,将其存储在NSNumber
实例中并返回。
如果结果是NSNumber
不支持的标量类型,转换为NSValue
对象并返回它。
6、如果所有其他方法都失败,则调用valueForUndefinedKey:
。这在默认情况下会引发一个异常,但是NSObject
的一个子类可能提供特定于键的行为。
下面例子演示了上面的执行步骤:
1 | @interface MyValidate : NSObject |
1 | NSLog(@"👉 %@", [obj valueForKey:@"num"]); // 👉 123 |
基本的 Setter 搜索模式
setValue:forKey:
的默认实现,给定key
和value
作为参数输入,尝试在KVC
对象内部设置一个名为key
的属性为value
(或者,对于非对象属性,为value
的解包装版本),执行以下过程:
1、按顺序查找名为set<Key>:
、_set<Key>
的第一个访问器。如果找到,将value
(或根据需要解包值)传入调用。否则,执行步骤2。
2、如果没有找到简单的访问器,并且类方法accessInstanceVariablesDirectly
返回值为YES
,则依次按名次 _<key>
、_is<Key>
、<key>
、is<Key>
查找一个实例变量。如果找到,直接用输入值value
(或展开值)设置变量并完成操作。
3、在没有找到访问器或实例变量时,则会调用setValue:forUndefinedKey:
,这在默认情况下会引发一个异常,但是NSObject
的一个子类可能提供特定于键的行为。
KVC 键值编码:是一种间接修改/读取对象属性的一种方法,kvc被称为cocoa的大招。在Apple 官方文档中,通过 Key-value coding 进行搜索。
有兴趣可以学习Apple官方文档中,关于
Mutable Array
、Mutable Ordered Set
、Mutable Set
的搜索模式。
KVC字典转模型
1 |
|
通过KVC
来实现字典转模型,其原理是先拿出字典里的Key
查找模型中的属性名,若找不到则会调用 setValue:forUndefinedKey:
方法默认抛出异常。