KVC
KVC定义
KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的。
在实现了访问器方法的类中,使用点语法和KVC访问对象其实差别不大,二者可以任意混用。但是没有访问起方法的类中,点语法无法使用,这时KVC就有优势了。
KVC的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject的类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的,因为没有继承NSObject),下面是KVC最为重要的四个方法:
- (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
NSKeyValueCoding类别中其他的一些方法:
+ (BOOL)accessInstanceVariablesDirectly;
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供属性值正确性�验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
扩展:
常用的点语法赋值,在底层是怎么实现赋值的。
点语法的赋值在底层实际是setter
在编译器通过
typedef struct {
float x, y, z;
} ThreeFloats;
@interface LGPerson : NSObject{
@public
NSString *myName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, strong) NSMutableArray *mArray;
@property (nonatomic, assign) int age;
@property (nonatomic) ThreeFloats threeFloats;
@property (nonatomic, strong) LGStudent *student;
LGPerson *person = [[LGPerson alloc] init];
// 一般setter 方法
person.name = @"LG_Cooci";
person.age = 18;
person->myName = @"cooci";
NSLog(@"%@ - %d - %@",person.name,person.age,person->myName);
1、KVC设置过程
1、Key-Value Coding (KVC) : 基本类型
基本的KVC设置,int要转成NSNumber类型。其他类型见文章最后。
[person setValue:@"KC" forKey:@"name"];
[person setValue:@19 forKey:@"age"];
[person setValue:@"酷C" forKey:@"myName"];
NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]);
2、 KVC - 集合类型 -
修改person的array,两种方式
- 直接取出array,然后重新赋值
- 用KVC将array转成 mutableArray,然后修改对应的值。
- 通常使用协议定义的三种代理对象访问代理方法,每种方法都有一个键和一个键路径变量:
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
返回行为类似于NSMutableArray
对象的代理对象。mutableSetValueForKey:
和mutableSetValueForKeyPath:
返回行为类似于NSMutableSet
对象的代理对象。mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
返回行为类似于NSMutableOrderedSet
对象的代理象。
person.array = @[@"1",@"2",@"3"];
// 由于不是可变数组 - 无法做到
// person.array[0] = @"100";
NSArray *array = [person valueForKey:@"array"];
// 用 array 的值创建一个新的数组
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);
// KVC 的方式
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"100";
NSLog(@"%@",[person valueForKey:@"array"]);
3、 KVC - 集合操作符
[self dictionaryTest];
[self arrayMessagePass];
//[self aggregationOperator];
//[self arrayOperator];
//[self arrayNesting];
//[self setNesting];
//[self arrayDemo];
#pragma mark - 字典操作
- (void)dictionaryTest{
NSDictionary* dict = @{
@"name":@"Cooci",
@"nick":@"KC",
@"subject":@"iOS",
@"age":@18,
@"length":@180
};
LGStudent *p = [[LGStudent alloc] init];
// 字典转模型
[p setValuesForKeysWithDictionary:dict];
NSLog(@"%@",p);
// 键数组转模型到字典
NSArray *array = @[@"name",@"age"];
NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
NSLog(@"%@",dic);
}
#pragma mark - KVC消息传递
- (void)arrayMessagePass{
NSArray *array = @[@"Hank",@"Cooci",@"Kody",@"CC"];
NSArray *lenStr= [array valueForKeyPath:@"length"];
NSLog(@"%@",lenStr);// 消息从array传递给了string
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"%@",lowStr);
}
4、KVC - 访问非对象属性 (比较重要)
访问非对象的属性,不能直接访问,需要使用中间类型。也就是先转换成 NSValue 类型然后再去存。读取的时候也需要先用NSValue接收然后再转换一下。
ThreeFloats floats = {1., 2., 3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslut = [person valueForKey:@"threeFloats"];
NSLog(@"%@",reslut);
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);
5、KVC - 层层访问
修改 person 的 student 属性,修改stubent的属性之后,然后将 student 赋值给 person的 student 属性。
@interface LGStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
LGStudent *student = [[LGStudent alloc] init];
student.subject = @"iOS";
person.student = student;
[person setValue:@"测试" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
setValue:forKey:
通过成员变量进行分析。用成员变量分析不用属性分析的原因是因为通过查看官方文档,可以知道set 和 成员变量都是影响KVC访问的因素,成员变量具有单一变量原则,属性会自动生成set方法。所以用成员变量来进行分析。
LGPerson.h
@interface LGPerson : NSObject{
@public
// NSString *_name;
// NSString *_isName;
// NSString *name;
// NSString *isName;
}
LGPerson.m
//MARK: - setKey. 的流程分析
//- (void)setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
//}
//- (void)_setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
//}
//- (void)setIsName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
//}
// 1: KVC - 设置值的过程
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
// NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
// NSLog(@"%@-%@",person->name,person->isName);
// NSLog(@"%@",person->isName);
- 去掉成员变量的注释,去掉set方法的注释,然后运行查看。然后逐个方法的查看注释,看看调用顺序
- 同理通过逐个注释成员变量,来看看 [person setValue:@"LG_Cooci" forKey:@"name"]; 赋值给成员变量的顺序
- 判断是否存在 1、set<key> 或者 2、_set<key> (掉下划线的属性) 3、setIs<key>,有的话执行。没有的话继续
- 如果没有条件1(简单访问方式)
- 判断accessInstanceVariablesDirectly(关闭或开启实例变量赋值) 是否存在 返回 YES。
- 如果返回的是NO,又不存在set方法的时候就会直接报错,报没有这个方法。
- 直接给这些实例变量 设值
- 判断 1、“_<key>” 2、“_is<key>” 3、“<key>” or 4、“is<key>”等实例变量
- 判断accessInstanceVariablesDirectly(关闭或开启实例变量赋值) 是否存在 返回 YES。
2、取值的过程
// 2: KVC - 取值的过程
person->_name = @"_name";
// person->_isName = @"_isName";
// person->name = @"name";
// person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
LGPerson.m
//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,
//- (NSString *)getName{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)name{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)isName{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)_name{
// return NSStringFromSelector(_cmd);
//}
同设置值一样的验证方式。验证结果:
- “get<key>”, “<key>”, “is<key>” or “_<key>” 如果有就跳到第五步
- 如果没有条件1开始 是否是NSArray 判断
- 是否是NSSet判断
- 非集合类型
- 如果找到,直接获取实例变量的值,然后继续执行步骤5
- 判断 “_<key>” “_is<key>” “<key>” or “is<key>”等实例变量
- 判断accessInstanceVariablesDirectly 是否存在 返回 YES
- 细节处理
- 如果结果是NSNumber不支持的标量类型,则转换为NSValue对象并返回
- 如果该值是NSNumber支持的标量类型,则将其存储在NSNumber实例中返回它。
- 如果检索到的属性值是对象指针,则只需返回结果。
- vlueForUnderfinedKey 报错
- 集合类型的还需要操作
3、集合运算符
当您发送与键值编码兼容的对象valueForKeyPath:
消息时,可以将集合运算符嵌入到键路径中。集合运算符是一小部分关键字之一,其后带有一个at符号(@),该符号指定getter
在返回数据之前应执行的操作以某种方式处理数据。由NSObject
提供的valueForKeyPath:
的默认实现会实现此行为。
@avg.属性名
求集合中对象某个属性的平均值。
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
@count
求集合中对象个数。@max.属性名
求集合中对象某个属性的最大值。@min.属性名
求集合中对象某个属性的最小值。@sum.属性名
求集合中对象某个属性的和。@distinctUnionOfObjects.属性名
取出集合中所有对象某个属性的值,并将这些值存入一个新的数组并返回。这个操作去重。@unionOfObjects.属性名
取出集合中所有对象某个属性的值,并将这些值存入一个新的数组并返回。这个操作不去重。@distinctUnionOfArrays.属性名
取出嵌套集合(集合嵌套集合)中所有对象某个属性的值,并返回一个新的数组。这个操作去重。@unionOfArrays.属性名
取出嵌套集合(集合嵌套集合)中所有对象某个属性的值,并返回一个新的数组。这个操作不去重。@distinctUnionOfSets.属性名
返回值是个一个NSSet
效果和distinctUnionOfArrays
一样。
#pragma mark - 聚合操作符
// @avg、@count、@max、@min、@sum
- (void)aggregationOperator{
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGStudent *p = [LGStudent new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
[personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"length"]);
/// 平均身高
float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
NSLog(@"%f", avg);
int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
NSLog(@"%d", count);
int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
NSLog(@"%d", sum);
int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
NSLog(@"%d", max);
int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
NSLog(@"%d", min);
}
4、类型转换
当调用getters如果返回值不是对象,则getter使用此值初始化NSNumber
对象(用于标量)或NSValue
对象(用于结构体),并返回该值。
类似地,默认情况下,使用setValue:forKey
之类的setter:在给定特定键的情况下,确定属性的访问器或实例变量所需的数据类型。 如果数据类型不是对象,则设置器首先将适当的<type> Value
消息发送到传入值对象以提取基础数据,然后存储该数据。
Data type | Creation method | Accessor method |
---|---|---|
BOOL |
numberWithBool: |
boolValue (in iOS) charValue (in macOS)* |
char |
numberWithChar: |
charValue |
double |
numberWithDouble: |
doubleValue |
float |
numberWithFloat: |
floatValue |
int |
numberWithInt: |
intValue |
long |
numberWithLong: |
longValue |
long long |
numberWithLongLong: |
longLongValue |
short |
numberWithShort: |
shortValue |
unsigned char |
numberWithUnsignedChar: |
unsignedChar |
unsigned int |
numberWithUnsignedInt: |
unsignedInt |
unsigned long |
numberWithUnsignedLong: |
unsignedLong |
unsigned long long |
numberWithUnsignedLongLong: |
unsignedLongLong |
unsigned short |
numberWithUnsignedShort: |
unsignedShort |
Data type | Creation method | Accessor method |
---|---|---|
NSPoint |
valueWithPoint: |
pointValue |
NSRange |
valueWithRange: |
rangeValue |
NSRect |
valueWithRect: (macOS only). |
rectValue |
NSSize |
valueWithSize: |
sizeValue |
5、自定义KVC
使用NSObject 的分类的形式,可以发现NSKeyValueCoding 也是写的是NSObject的一个分类,这是一种设计模式目的是为了解耦合。将KVC的功能写到NSObject的这个分类里面。
这里不做详细书写,自定义KVC是根据官方文档进行一步一步的去判断,实现。
6、KVC小技巧
typedef struct {
float x, y, z;
} ThreeFloats;
@interface LGPerson : NSObject{
@public
NSString *name;
NSString *_name;
NSString *_isName;
NSString *isName;
}
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) BOOL sex;
@property (nonatomic) ThreeFloats threeFloats;
1、
// 1: KVC 自动转换类型
NSLog(@"******1: KVC - int -> NSNumber - 结构体******");
//age是int类型,需要转成NSNumber
[person setValue:@18 forKey:@"age"];
// 上面那个表达 大家应该都会! 但是下面这样操作可以?
//由于age是int类型,即便是存的是用的字符串@“20”,KVC仍然能自动转换成NSCFNumber
[person setValue:@"20" forKey:@"age"]; // int - string
NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber
//由于sex是Bool类型,即便是存的是用的字符串@“20”,KVC仍然能自动转换成NSCFNumber
[person setValue:@"20" forKey:@"sex"];
NSLog(@"%@-%@",[person valueForKey:@"sex"],[[person valueForKey:@"sex"] class]);//__NSCFNumber
//threeFloats式结构体类型,存的时候需要存NSValue类型
ThreeFloats floats = {1., 2., 3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSLog(@"%@-%@",[person valueForKey:@"threeFloats"],[[person valueForKey:@"threeFloats"] class]);//NSConcreteValue
// 2: 设置空值
NSLog(@"******2: 设置空值******");
//设置age 会打印出“你傻不傻 。。。”,但是 subject 不会。是因为 setNilValueForKey 方法只能只对 NSNumber - NSValue的结构体进行处理,对NSString 不处理
[person setValue:nil forKey:@"age"]; // subject不会走 - 官方注释里面说只对 NSNumber - NSValue
[person setValue:nil forKey:@"subject"];
// 3: 找不到的 key
NSLog(@"******3: 找不到的 key******");
[person setValue:nil forKey:@"KC"];
// 4: 取值时 - 找不到 key
NSLog(@"******4: 取值时 - 找不到 key******");
NSLog(@"%@",[person valueForKey:@"KC"]);
// 5: 键值验证
NSLog(@"******5: 键值验证******");
NSError *error;
NSString *name = @"LG_Cooci";
if (![person validateValue:&name forKey:@"names" error:&error]) {
NSLog(@"%@",error);
}else{
NSLog(@"%@",[person valueForKey:@"name"]);
}
person.m
//设置age 会打印出“你傻不傻 。。。”,但是 subject 不会。是因为 setNilValueForKey 方法只能只对 NSNumber - NSValue的结构体进行处理,对NSString 不处理
- (void)setNilValueForKey:(NSString *)key{
NSLog(@"你傻不傻: 设置 %@ 是空值",key);
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"你瞎啊: %@ 没有这个key",key);
}
- (id)valueForUndefinedKey:(NSString *)key{
NSLog(@"你瞎啊: %@ 没有这个key - 给你一个其他的吧,别奔溃了!",key);
return @"Master 牛逼";
}
//MARK: - 键值验证 - 容错 - 派发 - 消息转发
//这里可以进行重定向,运行时进行容错,派发等
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
if([inKey isEqualToString:@"name"]){
[self setValue:[NSString stringWithFormat:@"里面修改一下: %@",*ioValue] forKey:inKey];
return YES;
}
*outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:10088 userInfo:nil];
return NO;
}