KVC

KVC基本概念

Key-Value Coding(简称KVC)是由 NSKeyValueCoding 非正式协议启用的一种机制,对象采用这种机制来提供对 其属性的间接访问。

当对象符合Key-Value Coding标准时,可以通过简洁、统一的 消息传递接口 通过 字符串参数 访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。

以下代码是我们经常使用KVC进行的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@interface Car : NSObject
@property (nonatomic, assign)NSInteger price;
@end

@class ZMCar;

// 定义一个ZMPerson类
@interface ZMPerson : NSObject
{
@private
int age;
}
@property (nonatomic, copy)NSString *name;
@property (nonatomic, strong)ZMCar *bus;
@property (nonatomic, copy, readonly)NSString *sex;

@end

@implementation ZMPerson
{
float _height;
}

- (instancetype)init {
if (self = [super init]) {
_sex = @"男生";
}
return self;
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ZMCar *c = [[ZMCar alloc] init];
ZMPerson *p = [[ZMPerson alloc] init];

//1.KVC直接给对象的属性赋值,并且自动判断其类型
[p setValue:@"zhangsan" forKey:@"name"];
[c setValue:@"21$" forKey:@"price"];
[p setValue:c forKey:@"bus"];

//2.KVC设置私有实例变量、@private的实例变量的值、readonly属性修饰的变量
[p setValue:@"22" forKey:@"age"];
[p setValue:@170 forKey:@"_height"];
[p setValue:@"女生" forKey:@"sex"];

//3.KVC通过字典给对象赋值
NSDictionary *dict = @{@"age":@33, @"name":@"李四", @"bus":c, @"_height":@"180"};
[p setValuesForKeysWithDictionary:dict];

//4.KVC和数组,根据keyPath获取数组中对象的属性的值
ZMCar *c1 = [[ZMCar alloc]init];
c1.price =21;
ZMCar *c2 = [[ZMCar alloc]init];
c2.price =22;
ZMCar *c3 = [[ZMCar alloc]init];
c3.price =23;

NSArray *arr = @[c1, c2, c3];

NSArray *carPriceArr = [arr valueForKeyPath:@"price"];
NSLog(@"%@", carPriceArr);

KVC的作用

通常,我们使用访问器方法访问对象的属性,如:getter方法 和 setter方法。在Objective-C中,也可以直接访问属性的底层实例变量。这些方式访问对象的属性都很简单,但需要调用特定于属性的方法或变量名。随着属性列表的增加或更改,访问这些属性的代码也必须随之增加,缺乏灵活性。相比之下,遵循 Key-Value Coding 的对象提供了一个简单的消息传递接口,该接口在其所有属性之间都是一致的。

Key-Value Coding 是其他许多Cocoa技术的基础概念,如:Key-value-observation(KVO)Cocoa bindingsCore DataAppleScript-ability。在某些情况下,KVC还可以帮助简化代码。

KVC可以执行的操作

NSObject 采用了 NSKeyValueCoding 协议,并为基本方法提供了默认实现。这样继承自NSObject的对象使其他对象能够通过紧凑的消息传递接口执行以下操作:

  • 访问对象的属性

  • 操作集合属性

  • 在集合对象上调用集合运算符

  • 访问非对象属性

  • 通过Key-Path访问属性

KVC访问对象的属性

Keykey path 标识对象的属性

键(Key)是标识特定属性的字符串。通常,按照约定,表示属性的键是在代码中显示的属性本身的名称。

键路径(key path)是一系列点分隔的键(Key),用于指定要遍历的对象(遵循KVC的层次结构对象)属性序列。

1
2
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

使用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface ZMPart : NSObject

@property (nonatomic, copy)NSString *name;
@property (nonatomic, strong)NSArray <NSString *>*place;

@end

@interface ZMTool : NSObject

@property (nonatomic, strong)NSNumber *objCount;
@property (nonatomic, strong)ZMPart *part;
@property (nonatomic, strong)NSArray <UIColor *> *colors;
@property (nonatomic, strong)NSArray <NSNumber *>*days;

@end
1
2
3
4
5
6
7
8
9
[tool setValue:@2 forKey:@"objCount"];
[tool setValue:@[[UIColor blueColor], [UIColor redColor]] forKey:@"colors"];
[tool setValue:[[ZMPart alloc] init] forKey:@"part"];
[tool setValue:@"new_part" forKeyPath:@"part.name"];

NSLog(@"%@, %@, %@", [tool valueForKey:@"objCount"], tool.colors, [tool valueForKeyPath:@"part.name"]);

[tool setValuesForKeysWithDictionary:@{@"objCount": @12, @"colors":@[[UIColor yellowColor]]}];
NSLog(@"%@", [tool dictionaryWithValuesForKeys:@[@"objCount", @"colors"]]);

KVC操作集合属性

符合 KVC 的对象,可以通过 其协议提供的方法来操作集合类型的属性内容。协议提供了如下方法:

  • mutableArrayValueForKey:mutableArrayValueForKeyPath:
    返回行为类似于NSMutableArray对象的代理对象

  • mutableSetValueForKey:mutableSetValueForKeyPath:
    返回行为类似于NSMutableSet对象的代理对象

  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:
    返回行为类似于NSMutableOrderedSet对象的代理对象

通过如下例子来理解代理对象:

1
2
3
4
5
6
7
[tool setValue:@[@1, @2, @3] forKey:@"days"];
NSLog(@"%@", [tool valueForKey:@"days"]); // 打印:[1, 2, 3]

// 通过代理对象操作集合类型的属性:
NSMutableArray *days = [tool mutableArrayValueForKey:@"days"];
[days removeObject:@2];
NSLog(@"%@", [tool valueForKey:@"days"]); // 打印:[1, 3]
1
2
3
4
5
6
[tool setValue:@[@"北京", @"河北"] forKeyPath:@"part.place"];
NSLog(@"%@", [tool valueForKeyPath:@"part.place"]); // 打印:["北京", "河北"]

NSMutableArray *mutPlace = [tool mutableArrayValueForKeyPath:@"part.place"];
[mutPlace addObject:@"河南"];
NSLog(@"%@", [tool valueForKeyPath:@"part.place"]); // 打印:["北京", "河北", "河南"]

通过对代理对象进行操作集合的内容,协议的默认实现将相应地修改底层属性。这比valueForKey:获取不可变的集合对象,然后创建一个内容已修改的对象,最后使用setValue:forKey:存回更有效。

这些方法的另一个好处是可以维护集合对象中保存的对象的KVO遵从性。

使用集合运算符

当向符合KVC的对象发送 valueForKeyPath:消息时,可以在 Key-Path 中嵌入 集合运算符

集合运算符(Collection Operators)是前面带有一个@符号一小串关键字中的一个,它指定了getter在返回数据之前应该执行的操作,以某种方式对数据进行操作。NSObject默认实现实现了此行为。

Key-Path包含集合运算符时,使用格式如下:

keypath

  • Left Key Path:称为左键路径,它指集合运算符之前的Key-Path的任何部分,指示相对于消息接收方要操作的集合。如果将消息直接发送给一个集合对象,比如NSArray实例,则可能会省略左键路径。

  • Right Key Path:称为右键路径,它指集合运算符后面的Key-Path部分,指定了该运算符应处理的集合中的属性。除@count之外的所有集合运算符都需要一个正确的Key-Path

Collection Operators的三种基本行为类型:

  1. Aggregation Operators(聚合运算符),以某种方式合并集合对象,并返回一个通常与右键路径中指定的属性的数据类型匹配的单个对象。@count运算符符是个例外,它不接受右键路径,并且总是返回一个NSNumber实例。
  2. Array Operators(数组运算符),返回一个NSArray实例,该实例包含命名集合中包含的某些对象子集。
  3. Nesting Operators(嵌套运算符),在包含其他集合的集合上工作,并根据运算符返回NSArrayNSSet实例,该实例以某种方式组合嵌套集合的对象。

下面例子均使用以下对象进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@interface Person : NSObject

@property (nonatomic, copy)NSString *name;
@property (nonatomic, strong)NSNumber *age;
@property (nonatomic, strong)NSDate *birthday;

+ (instancetype)personWithDict:(NSDictionary *)dict;

@end

@implementation Person

- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}

+ (instancetype)personWithDict:(NSDictionary *)dict {
return [[self alloc] initWithDict:dict];
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"😭😭😭😭未找到key: %@", key);
}

@end

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
self.peoples = @[[Person personWithDict:@{@"name":@"张三", @"age":@18, @"birthday":@"2001-02-22"}],
[Person personWithDict:@{@"name":@"李四", @"age":@22, @"birthday":@"2001-01-23"}],
[Person personWithDict:@{@"name":@"李四", @"age":@22, @"birthday":@"2001-01-23"}],
[Person personWithDict:@{@"name":@"赵六", @"age":@19, @"birthday":@"1998-03-23"}]
];

NSNumber *average = [self.peoples valueForKeyPath:@"@avg.age"];
NSLog(@"%@", average); // 打印:20.25

NSNumber *number = [self.peoples valueForKeyPath:@"@count"];
NSLog(@"%@", number); // 打印:4

NSDate *max = [self.peoples valueForKeyPath:@"@max.birthday"];
NSLog(@"%@", max); // 打印: 2001-02-22

NSDate *min = [self.peoples valueForKeyPath:@"@min.birthday"];
NSLog(@"%@", min); // 打印: 1998-03-23

NSNumber *sum = [self.peoples valueForKeyPath:@"@sum.age"];
NSLog(@"%@", sum); // 打印:81

Array Operators

数组运算符 使valueForKeyPath:返回一个对象数组,该对象数组与右键路径指示的一组特定对象相对应。

如果使用数组运算符时有任何子对象为nil,则valueForKeyPath:方法将引发异常。

  • @distinctUnionOfObjects运算符,valueForKeyPath:创建并返回一个数组,该数组包含集合中与由右键路径指定的属性相对应的不重复对象。

  • @unionOfObjects运算符,与@distinctUnionOfObjects类似,但是返回的数组没有删除重复的对象。

1
2
3
4
5
NSArray *distinct = [self.peoples valueForKeyPath:@"@distinctUnionOfObjects.name"];
NSLog(@"去掉重复:%@", distinct); // 打印:去掉重复:["赵六", "张三", "李四"]

NSArray *unionNames = [self.peoples valueForKeyPath:@"@unionOfObjects.name"];
NSLog(@"获取所有:%@", unionNames); // 打印:获取所有:["张三", "李四", "李四", "赵六"]

Nesting Operators

嵌套运算符 对嵌套的集合进行操作,其中集合本身的每个条目都包含一个集合。

使用嵌套运算符时,如果任何叶子对象为nil,则 valueForKeyPath: 方法将引发异常。

  • @distinctUnionOfArrays运算符,valueForKeyPath:创建并返回一个数组,该数组包含与右键路径指定的属性相对应的所有集合的组合的不重复的对象。

  • @unionOfArrays运算符,与@distinctUnionOfArrays 运算符类似,但不会删除重复的对象。

  • @distinctUnionOfSets运算符,valueForKeyPath:创建并返回一个NSSet对象,该对象包含与右键路径指定的属性相对应的所有集合的组合的不重复的对象。但是它要求操作的是的NSSet类型的实例。

1
2
3
4
5
6
7
8
9
10
11
self.boy = @[[Person personWithDict:@{@"name":@"张三", @"age":@18, @"birthday":@"2001-02-22"}],
[Person personWithDict:@{@"name":@"李四", @"age":@22, @"birthday":@"2001-01-23"}],
[Person personWithDict:@{@"name":@"李四", @"age":@22, @"birthday":@"2001-01-23"}],
[Person personWithDict:@{@"name":@"赵六", @"age":@19, @"birthday":@"1998-03-23"}]
];

self.girl = @[[Person personWithDict:@{@"name":@"小红", @"age":@18, @"birthday":@"2001-02-22"}],
[Person personWithDict:@{@"name":@"小红", @"age":@22, @"birthday":@"2001-01-23"}],
[Person personWithDict:@{@"name":@"小翠", @"age":@22, @"birthday":@"2001-01-23"}],
[Person personWithDict:@{@"name":@"小丹", @"age":@19, @"birthday":@"1998-03-23"}]
];
1
2
3
4
5
6
7
8
9
NSArray *somePeople = @[self.boy, self.girl];

NSArray *distinctArr = [somePeople valueForKeyPath:@"@distinctUnionOfArrays.birthday"];
// 打印:去掉n个数组中重复:[1998-03-23, 2001-01-23, 2001-02-22]
NSLog(@"去掉n个数组中重复:%@", distinctArr);

NSArray *unionArr = [somePeople valueForKeyPath:@"@unionOfArrays.birthday"];
// 打印:获取n个数组中所有:[2001-02-22, 2001-01-23, 2001-01-23, 1998-03-23, 2001-02-22, 2001-01-23, 2001-01-23,1998-03-23]
NSLog(@"获取n个数组中所有:%@", unionArr);

访问非对象属性

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异常,一般子类可能会覆盖这个行为。

下面例子演示了利用KVCnon-object properties(非对象属性)的访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct {
float x, y, z;
} ThreePoint;

@interface MyScalar : NSObject

@property (nonatomic, assign)float num;
@property (nonatomic, assign)BOOL result;
@property (nonatomic, assign)CGPoint point;
@property (nonatomic, strong)NSNumber *count;
@property (nonatomic, assign)int total;
@property (nonatomic, assign)ThreePoint threePt;

@end

MyScalar *scalar = [[MyScalar alloc] init];

使用NSNumber实例包装的标量类型,NSValue封装结构体类型。

1
2
3
4
5
6
7
8
9
10
11
[scalar setValue:[NSNumber numberWithFloat:0.25] forKey:@"num"];
NSNumber *num = [scalar valueForKey:@"num"];
NSLog(@"%0.2f, %@", num.floatValue, [num class]); // 0.25, __NSCFNumber

[scalar setValue:[NSNumber numberWithBool:false] forKey:@"result"];
NSNumber *result = [scalar valueForKey:@"result"];
NSLog(@"%d, %@", result.boolValue, [result class]); // 0, __NSCFBoolean

[scalar setValue:[NSValue valueWithCGPoint:CGPointMake(50.0, 50.0)] forKey:@"point"];
NSValue *point = [scalar valueForKey:@"point"];
NSLog(@"%@, %@", NSStringFromCGPoint(point.CGPointValue), [point class]); // {50, 50}, NSConcreteValue

通过KVC把非对象属性设置为nil,会向KVC对象发送setNilValueForKey:消息,并抛出异常:

1
2
3
4
5
6
7
[scalar setValue:nil forKey:@"count"];
NSNumber *count = [scalar valueForKey:@"count"];
NSLog(@"😄 %@", count); // 😄 (null)

// [scalar setValue:nil forKey:@"total"]; // 抛异常Terminating app due to uncaught exception 'NSInvalidArgumentException'
NSNumber *total = [scalar valueForKey:@"total"];
NSLog(@"😭 %@", total); // 😭 0

利用KVC访问自定义struct类型的属性:

1
2
3
4
5
6
7
8
ThreePoint three = {1.2, 2.2, 2.3};
NSValue *value = [NSValue valueWithBytes:&three objCType:@encode(ThreePoint)];
[scalar setValue:value forKey:@"threePt"];

NSValue *threePt = [scalar valueForKey:@"threePt"];
ThreePoint getThree;
[threePt getValue:&getThree];
NSLog(@"%0.2f, %0.2f, %0.2f, %@", getThree.x, getThree.y, getThree.z, [threePt class]); // 1.20, 2.20, 2.30, NSConcreteValue

验证属性

KVC的协议中定义了支持属性验证的方法,即对属性值的合法性进行校验,通过如下两个方法实现的:

1
2
3
4
5
6
7
- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue
forKey:(NSString *)inKey
error:(out NSError *__autoreleasing _Nullable *)outError

- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue
forKeyPath:(NSString *)inKeyPath
error:(out NSError *__autoreleasing _Nullable *)outError

当你调用上面的验证方法时,KVO的默认实现将在接收验证消息的对象(或keyPath中最后的对象)中搜索一种方法,其方法名与validate<Key>:error:模式匹配。

  • 如果对象没有这样的方法,则默认验证成功,返回YES
  • 当特定于属性的验证方法(validate<Key>:error:)存在时,默认实现将返回调用该方法的结果。

由于validate<Key>:error:方法通过引用接收值(value)和错误参数(error),因此验证有三种可能的结果:

  • 验证方法认为value有效,并返回YES,而不改变valueerror
  • 验证方法认为value无效,但并不修改它,返回NO,并将error设置为NSError对象。
  • 验证方法认为value无效,创建一个新的有效对象替换value。方法返回YES,同时保持error对象不变。

如下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@interface MyValidate : NSObject

@property (nonatomic, copy)NSString *name;

@end

@implementation MyValidate

// 验证属性 name 的值是否为 `admin`
- (BOOL)validateName:(inout id *)value error:(out NSError *__autoreleasing _Nullable *)outError {

NSString *name = *value;
BOOL res = NO;

if ([name isEqualToString:@"admin"]) {
res = YES;
} else {

// NSError *err = [[NSError alloc] initWithDomain:NSCocoaErrorDomain code:100 userInfo:@{NSLocalizedDescriptionKey:@"描述:设置name值不符合要求",
// NSLocalizedFailureReasonErrorKey:@"原因:name的值必须为admin",
// NSLocalizedRecoverySuggestionErrorKey:@"建议:修改name的值"}];
// res = NO;
// *outError = err;

*value = @"admin";
res = YES;
}
return res;
}

@end
1
2
3
4
5
6
7
8
MyValidate *obj = [[MyValidate alloc] init];
NSError *error;
NSString *value = @"tom";
BOOL res = [obj validateValue:&value forKey:@"name" error:&error];
if (res) {
[obj setValue:value forKey:@"name"];
}
NSLog(@"%d, %@, %@, %@", res, value, error, [obj valueForKey:@"name"]); // 1, admin, (null), admin

通常,KVC的默认实现都没有定义任何自动执行验证的机制,一般我们在合适的地方提供验证方法。

通常,仅在Objective-C中使用此处描述的验证在Swift中,属性验证更习惯地通过依赖编译器对 Optional 和 强类型 检查的支持来处理,而使用willSetdidSet来测试运行时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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@interface MyValidate : NSObject
{
NSString *_funName;
}
@end

@implementation MyValidate


- (instancetype)init {
if (self = [super init]) {
_funName = @"ABC";
}
return self;
}

// 步骤1
- (NSNumber *)getNum {
return @123;
}

- (void)setNum:(NSNumber *)num {

}

// 步骤2
// 决定代理数组元素的个数
- (NSUInteger)countOfArr {
return 3;
}

// 该方法返回值依次作为代理数组的元素
// [ [123,], [456,], [???,] ]
//- (NSArray *)objectInArrAtIndex:(NSUInteger)index {
// switch (index) {
// case 0: return @[@"123"];
// case 1: return @[@"456"];
// default: return @[@"???"];
// }
//}


// 如果未实现 objectIn<Key>AtIndex: 则此方法会被调用,返回值为数组类型,依次取该数组的第一个元素作为代理数组的元素
// [123, 456, ???]
- (NSArray *)arrAtIndexes:(NSIndexSet *)indexes {
switch (indexes.firstIndex) {
case 0: return @[@"123", @"a"];
case 1: return @[@"456", @"b"];
default: return @[@"???", @"c"];
}
}


// 步骤3
- (NSUInteger)countOfAnimal {
return 2;
}

// 返回值必须为 NSEnumerator 类型的枚举器,元素个数与 countOfAnimal 必须对应
- (NSEnumerator *)enumeratorOfAnimal {
NSSet *set = [[NSSet alloc] initWithObjects:@"abc",@"123", nil];
NSEnumerator *enumerator = [set objectEnumerator];
return enumerator;
}

- (NSString *)memberOfAnimal:(id)index {
return @"xxx";
}


// 步骤4
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}

@end
1
2
3
4
5
6
7
NSLog(@"👉 %@", [obj valueForKey:@"num"]);  // 👉 123

NSLog(@"👉 %@", [obj valueForKey:@"arr"]); //👉 [123,456,???,]

NSLog(@"👉 %@", [obj valueForKey:@"animal"]); // 👉 {(abc, 123)}

NSLog(@"👉 %@", [obj valueForKey:@"funName"]); // 👉 ABC

基本的 Setter 搜索模式

setValue:forKey:的默认实现,给定keyvalue作为参数输入,尝试在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 ArrayMutable Ordered SetMutable Set的搜索模式。

KVC字典转模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

@interface Status : NSObject

@property (nonatomic, assign)NSInteger ID; // 用ID来接收字典中的id
@property (nonatomic, strong) NSString *source;
@property (nonatomic, assign) int reposts_count;

+ (__kindof Status *)statusWithDict:(NSDictionary *)dict;

@end

@implementation Status
//kvc实现字典转模型
- (instancetype)initWithDict:(NSDictionary *)dict{
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}

+ (__kindof Status *)statusWithDict:(NSDictionary *)dict {
return [[self alloc]initWithDict:dict];
}

// 避免保证未找到key时,抛出异常程序崩溃
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if ([key isEqualToString:@"id"]) {
self.ID = value;
} else {
NSLog(@"为找到key=%@", key);
}
}
@end

通过KVC来实现字典转模型,其原理是先拿出字典里的Key查找模型中的属性名,若找不到则会调用 setValue:forUndefinedKey: 方法默认抛出异常。

学习博客

About Key-Value Coding

Cocoa Core Competencies

耍耍iOS中的KVC

iOS 底层探索 - KVC

文章作者: Czm
文章链接: http://yoursite.com/2020/09/15/KVC/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm