一、简介
KVC 的全称是 Key-Value Coding(键值编码),是由 NSKeyValueCoding
非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问,可以通过字符串来访问一个对象的成员变量或其关联的存取方法(getter or setter
)。
通常,我们可以直接通过存取方法或变量名来访问对象的属性。我们也可以使用 KVC 间接访问对象的属性,并且 KVC 还可以访问私有变量。某些情况下 KVC 还可以帮助简化代码。
二、使用
1.访问对象属性
(1)常用 API
- (nullable id)valueForKey:(NSString *)key; // 通过 key 来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通过 keyPath 来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; // 通过 key 来赋值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通过 keyPath 来赋值
(2)基础操作
#import
#import "DJTestModelOne.h"
@interface DJTestModel : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *test;
@property (nonatomic,strong) DJTestModelOne *one;
@end
#import
@interface DJTestModelOne : NSObject
@property (nonatomic,copy)NSString *oneName;
@end
#import "ViewController.h"
#import "DJTestModel.h"
#import "DJTestModelOne.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
DJTestModel *model = [[DJTestModel alloc]init];
//直接赋值
[model setName:@"dj"];
//使用 KVC 赋值
[model setValue:@"dj" forKey:@"name"];
DJTestModelOne *oneModel = [[DJTestModelOne alloc]init];
model.one = oneModel;
//使用 KVC 的 keyPath 赋值
[model setValue:@"djOne" forKeyPath:@"one.oneName"];
//使用 KVC 取值
NSString *name = [model valueForKey:@"name"];
//使用 KVC 的 keyPath 取值
NSString *oneName = [model valueForKeyPath:@"one.oneName"];
}
@end
KVC 还支持多级访问,KeyPath
用法跟点语法相同。
(3)多值操作
给定一组 Key,获得一组 value,以字典的形式返回。该方法为数组中的每个 Key 调用 valueForKey:
方法。
[model setValuesForKeysWithDictionary:@{@"name":@"djValue",@"test":@"testValue"}];
将指定字典中的值设置到消息接收者的属性中,使用字典的 Key 标识属性。默认实现是为每个键值对调用 setValue:forKey:
方法 ,会根据需要用 nil
替换 NSNull
对象。
NSDictionary *dict = [model dictionaryWithValuesForKeys:@[@"name",@"test"]];
2.访问集合属性
我们可以像访问其它对象一样使用 valueForKey:
或 setValue:forKey:
方法来获取或设置集合对象(主要指 NSArray
和 NSSet
)。但是,当我们要操作集合对象的内容,比如添加或者删除元素时,通过 KVC 的可变代理方法获取集合代理对象是最有效的。
【扩展:根据 KVO 的实现原理,是在运行时动态生成子类并重写 setter
方法来达到可以通知所有观察者对象的目的,因此我们对集合对象进行操作是不会触发 KVO 的。当我们要使用 KVO 监听集合对象变化时,需要通过 KVC 的可变代理方法获取集合代理对象,然后对代理对象进行操作。当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。】
KVC 提供了三种不同的代理对象访问的代理方法,每种都有 Key 和 KeyPath 两种方法。
-
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
返回NSMutableArray
对象的代理对象。 -
mutableSetValueForKey:
和mutableSetValueForKeyPath:
返回NSMutableSet
对象的代理对象。 -
mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
返回NSMutableOrderedSet
对象的代理对象。
3.使用集合运算符
KVC 的 valueForKeyPath:
方法除了可以取出属性值以外,还可以在 KeyPath 中嵌套集合运算符,来对集合对象进行操作。
以下是KeyPath集合运算符的格式,主要分为 3 个部分:
- Left key path:左键路径,要操作的集合对象,如果消息接收者就是集合对象,则可以省略 Left 部分;
- Collection operator:集合运算符;
- Right key path:右键路径,要进行运算的集合中的属性。
集合运算符主要分为三类:
- 聚合运算符:以某种方式合并集合中的对象,并返回右键路径中指定的属性的数据类型匹配的一个对象,一般返回
NSNumber
实例。 - 数组运算符:根据运算符的条件,将符合条件的对象以一个
NSArray
实例返回。 - 嵌套运算符:处理集合对象中嵌套其他集合对象的情况,并根据运算符返回一个
NSArray
或NSSet
实例。
示例:
#import
#import "DJTestModelOne.h"
@interface DJTestModel : NSObject
@property (nonatomic,strong) NSArray *oneArray;
@end
#import
@interface DJTestModelOne : NSObject
@property (nonatomic,assign)NSInteger num;
@property (nonatomic,copy)NSString * type;
@end
(1)使用聚合运算符
#import "ViewController.h"
#import "DJTestModel.h"
#import "DJTestModelOne.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
DJTestModel *model = [[DJTestModel alloc]init];
DJTestModelOne *one1 = [[DJTestModelOne alloc]init];
one1.num = 1;
DJTestModelOne *one2 = [[DJTestModelOne alloc]init];
one2.num = 2;
DJTestModelOne *one3 = [[DJTestModelOne alloc]init];
one3.num = 3;
DJTestModelOne *one4 = [[DJTestModelOne alloc]init];
one4.num = 4;
model.oneArray = @[one1,one2,one3,one4];
NSNumber *avg = [model.oneArray valueForKeyPath:@"@avg.num"];
NSNumber *conunt = [model.oneArray valueForKeyPath:@"@count"];
NSNumber *sum = [model.oneArray valueForKeyPath:@"@sum.num"];
NSNumber *max = [model.oneArray valueForKeyPath:@"@max.num"];
NSNumber *min = [model.oneArray valueForKeyPath:@"@min.num"];
}
@end
(2)使用数组运算符
#import "ViewController.h"
#import "DJTestModel.h"
#import "DJTestModelOne.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
DJTestModel *model = [[DJTestModel alloc]init];
DJTestModelOne *one1 = [[DJTestModelOne alloc]init];
one1.type = @"one1";
DJTestModelOne *one2 = [[DJTestModelOne alloc]init];
one2.type = @"one2";
DJTestModelOne *one3 = [[DJTestModelOne alloc]init];
one3.type = @"one2";
DJTestModelOne *one4 = [[DJTestModelOne alloc]init];
one4.type = @"one4";
model.oneArray = @[one1,one2,one3,one4];
//获取集合中的所有 type 对象
NSArray *types = [model.oneArray valueForKeyPath:@"@unionOfObjects.type"];
// 获取集合中的所有不同的 type 对象
NSArray *disTypes = [model.oneArray valueForKeyPath:@"@distinctUnionOfObjects.type"];
}
在使用数组运算符时,如果有任何操作的对象为 nil
,则 valueForKeyPath:
方法将引发异常。
(3)使用嵌套运算符
处理集合对象中嵌套其他集合对象的情况,并根据运算符返回一个NSArray或NSSet实例。
NSArray *arrayOfArrays = @[model.oneArray,model.oneArray];
NSArray *arrayTypes = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.type"];
NSArray *distinctTypes = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.type"];
- 在使用嵌套运算符时,
valueForKeyPath:
内部会根据运算符创建一个NSMutableArray
或NSMutableSet
对象,将集合中的 array 和 set 添加进去再进行操作。如果集合中有非集合元素,会导致 Crash。 - 使用
unionOfArrays
或distinctUnionOfArrays
运算符,消息接收者应该是arrayOfArrays
类型,即NSArray< NSArray* >* arrayOfArrays;
使用distinctUnionOfSets
运算符,消息接收者应该是setOfSets
或者arrayOfSets
类型。否则会发生异常。 - 在使用嵌套运算符时,如果有任何操作的对象为
nil
, 则valueForKeyPath:
方法将引发异常。
(4)拓展
如果集合中的对象都是 NSNumber
,右键路径可以用 self
。
NSArray *array = @[@1, @2, @3, @4, @5];
NSNumber *sum = [array valueForKeyPath:@"@sum.self"];
NSLog(@"%d",[sum intValue]);
4.非对象值处理
KVC 支持基础数据类型和结构体,在使用 KVC 进行赋值或取值的时候,会自动在非对象值和对象值之间进行转换。
- 当进行取值如
valueForKey:
时,如果返回值非对象,会使用该值初始化一个NSNumber
(用于基础数据类型)或NSValue
(用于结构体)实例,然后返回该实例。 - 当进行赋值如
setValue:forKey:
时,如果 key 的数据类型非对象,则会发送一条
消息给 value 对象以提取基础数据,然后赋值给 key。Value
注意:因为Swift 中的所有属性都是对象,所以这里仅适用于 Objective-C 属性。
当进行赋值如 setValue:forKey:
时,如果 key 的数据类型是非对象类型,则 value 就禁止传 nil
。否则会调用 setNilValueForKey:
方法,该方法的默认实现抛出异常 NSInvalidArgumentException
,并导致程序 Crash。
5.属性验证
KVC 提供了属性验证的方法,如下。我们可以在使用 KVC 赋值前验证能否为这个 key 赋值指定 value。
validateValue
方法的默认实现是查看消息接收者类中是否实现了遵循命名规则为 validate
的方法,如果有的话就返回调用该方法的结果;如果没有的话,则默认验证成功并返回 YES。我们可以在消息接收者类中实现 validate
的方法来自定义逻辑返回 YES 或 NO。
#import
#import "DJTestModelOne.h"
@interface DJTestModel : NSObject
@property (nonatomic,strong) NSArray *oneArray;
@property (nonatomic,copy)NSString *name;
@end
#import "DJTestModel.h"
@implementation DJTestModel
-(BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
NSString *name = *ioValue;
BOOL result = NO;
if ([name isEqualToString:@"dj"]) {
result = YES;
}
return result;
}
@end
DJTestModel *model = [[DJTestModel alloc]init];
NSString *value = @"dj";
NSString *key = @"name";
NSError *error;
BOOL result = [model validateValue:&value forKey:key error:&error];
if (error) {
NSLog(@"error = %@", error);
}
三、原理
1.搜索规则
除了了解 KVC 的使用,了解 KVC 取值和赋值过程的工作原理也是很有必要的。
(1)基本的 Getter 搜索模式
以下是 valueForKey:
方法的默认实现,给定一个 key 作为输入参数,在消息接收者类中操作,执行以下过程。
- 第一步:按照
get
、
、is
、_
顺序查找方法。如果找到就调用取值并执行第五步,否则执行第二步; - 第二步:查找
countOf
、objectIn
、AtIndex:
命名的方法。如果找到第一个和后面两个中的至少一个,则创建一个能够响应所有AtIndexes: NSArray
的方法的集合代理对象(类型为NSKeyValueArray
,继承自NSArray
),并返回该对象。否则执行第三步;
代理对象随后将其接收到的任何NSArray
消息转换为countOf
、objectIn
、AtIndex:
消息的组合,并将其发送给 KVC 调用方。如果原始对象还实现了一个名为AtIndexes: get
的可选方法,则代理对象也会在适当时使用该方法。:range:
当 KVC 调用方与代理对象一起工作时,允许底层属性的行为如同NSArray
一样,即使它不是NSArray
。 - 第三步:查找
countOf
、enumeratorOf
、memberOf
命名的方法。如果三个方法都找到,则创建一个能够响应所有: NSSet
的方法的集合代理对象(类型为NSKeyValueSet
,继承自NSSet
),并返回该对象。否则执行第四步;
代理对象随后将其接收到的任何NSSet
消息转换为countOf
、enumeratorOf
、memberOf
消息的组合,并将其发送给 KVC 调用方。:
当 KVC 调用方与代理对象一起工作时,允许底层属性的行为如同NSSet
一样,即使它不是NSSet
。 - 第四步:查看消息接收者类的
+accessInstanceVariablesDirectly
方法的返回值(默认返回YES
)。如果返回YES
,就按照_
、_is
、
、is
顺序查找成员变量。如果找到就直接取值并执行第五步,否则执行第六步。如果+accessInstanceVariablesDirectly
方法返回NO
也执行第六步。 - 第五步:如果取到的值是一个对象指针,即获取的是对象,则直接将对象返回。如果取到的值是一个
NSNumber
支持的数据类型,则将其存储在NSNumber
实例并返回。如果取到的值不是一个NSNumber
支持的数据类型,则转换为NSValue
对象, 然后返回。 - 第六步:调用
valueForUndefinedKey:
方法,该方法抛出异常NSUnknownKeyException
,并导致程序 Crash。这是默认实现,我们可以重写该方法根据特定 key 做一些特殊处理。
(2)基本的 Setter 搜索模式
以下是 setValue:forKey:
方法的默认实现,给定 key 和 value 作为输入参数,尝试将 KVC 调用方的属性名为 ke y的值设置为 value,执行以下过程。
- 第一步:按照
set
、: _set
顺序查找方法。如果找到就调用并将 value 传进去(根据需要进行数据类型转换),否则执行第二步。: - 第二步:查看消息接收者类的
+accessInstanceVariablesDirectly
方法的返回值(默认返回YES
)。如果返回YES
,就按照_
、_is
、
、is
顺序查找成员变量(同基本的 Getter 搜索模式)。如果找到就将 value 赋值给它(根据需要进行数据类型转换),否则执行第三步。如果+accessInstanceVariablesDirectly
方法返回NO
也执行第三步。 - 第三步:调用
setValue:forUndefinedKey:
方法,该方法抛出异常NSUnknownKeyException
,并导致程序 Crash。这是默认实现,我们可以重写该方法根据特定 key 做一些特殊处理。
2.异常处理
根据 KVC 搜索规则,当没有搜索到对应的 key 或者 keyPath 相关方法或者变量时,会调用对应的异常方法 valueForUndefinedKey:
或 setValue:forUndefinedKey:
,这两个方法的默认实现是抛出异常 NSUnknownKeyException
,并导致程序 Crash。我们可以重写这两个方法来处理异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
当进行赋值如 setValue:forKey:
时,如果 key 的数据类型是非对象类型,则 value 就禁止传 nil
。否则会调用 setNilValueForKey:
方法,该方法的默认实现是抛出异常 NSInvalidArgumentException
,并导致程序 Crash。我们可以重写这个方法来处理异常。
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:@"hidden"]) {
[self setValue:@(NO) forKey:@”hidden”];
} else {
[super setNilValueForKey:key];
}
}