键值编码(KVC)
KVC(Key Value Coding)是一种允许以字符串形式间接操作对象属性的方式。
最基本的KVC是由NSKeyValueCoding协议提供支持,最基本的操作属性如下:
- setValue: 属性值 forKey: 属性名:为指定属性设置值;
- valueForKey: 属性名:获取指定属性的值
代码演示:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AUser : NSObject
@property (nonatomic, copy) NSString *str1;
@property (nonatomic, copy) NSString *str2;
@end
NS_ASSUME_NONNULL_END
#import <Foundation/Foundation.h>
#import "AUser.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
AUser *aUser = [[AUser alloc] init];
//使用KVC方式为str1属性设置值
[aUser setValue:@"astr11" forKey:@"str1"];
//使用KVC方式为str2属性设置值
[aUser setValue:@"astr22" forKey:@"str2"];
//使用KVC方式获取AUser对象的属性值
NSLog(@"str1: %@", [aUser valueForKey:@"str1"]);
NSLog(@"str2: %@", [aUser valueForKey:@"str2"]);
}
return 0;
}
结果:
在使用KVC时,都是通过字符串来指定被操作的属性。即使用forKey传入属性名的字符串。
对于setValue: forKey: 方法,底层的执行机制如下:
- 程序优先考虑调用属性的setter方法
- 如果该类没有setter方法,KVC机制会搜索该类中名为传入的“_该字符串”的成员变量(大部分时候即创建属性的时候自动创建的成员变量)无论该成员变量是在接口或者实现部分定义、无论它用哪个访问控制符修饰,这条KVC底层上是对该成员变量的赋值。
如果该类即没有setter方法也没有“_name”成员变量,那么KVC机制会搜索该类中名为name的成员变量(大部分时候即我们自己定义的成员变量)(与上条一样)
如果上面3步都没有找到,那么系统会执行该对象的setValue: forUndefinedKey:方法,该方法的实现就是引发一个异常,导致程序结束
valueForKey方法其他与上面一样,但是它获取的是getter方法的返回值。没有找到成员变量会执行valueForUndefinedKey:方法,该方法也会引起异常导致程序关闭。
代码举例:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AUser : NSObject {
@package
NSString *name;
NSString *_name;
}
@end
NS_ASSUME_NONNULL_END
#import "AUser.h"
@implementation AUser {
int age;
}
@end
#import <Foundation/Foundation.h>
#import "AUser.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
AUser *aUser = [[AUser alloc] init];
//使用KVC给属性赋值,KVC的搜素顺序为:
//1、setName方法;2、_name成员变量;3、name成员变量
//因此,在此处我们是先搜索到了_name成员变量,所以是给_name赋了值,name没有赋值
//因此name为空
[aUser setValue:@"strName1" forKey:@"name"];
NSLog(@"name = %@", aUser->name);
NSLog(@"_name = %@", aUser->_name);
//虽然age成员变量是在实现部分定义的,但是它还是会被赋值
[aUser setValue: [NSNumber numberWithInt:5] forKey:@"age"];
NSLog(@"age = %@", [aUser valueForKey:@"age"]);
}
return 0;
}
处理不存在的Key
前面说过,使用KVC时,如果该属性没有setter、getter方法,也不存在对应的成员变量时,程序会调用setValue: forUndefinedKey:或valueForUndefinedKey:方法。系统默认该方法的实现是引发一个异常然后结束程序,但是我们可以重写这个方法,使其达到我们想要的效果。
只需要在FKApple类实现部分重写setValue:forUndefinedKey:方法,甚至不需要在类接口
声明该方法,例:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AUser : NSObject {
@package
NSString *name;
NSString *_name;
}
@end
NS_ASSUME_NONNULL_END
#import "AUser.h"
- (void) setValue: (id)value forUndefinedKey:(nonnull NSString *)key {
NSLog(@"重写了setValue:value forUndefinedKey方法");
}
@end
#import <Foundation/Foundation.h>
#import "AUser.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
AUser *aUser = [[AUser alloc] init];
[aUser setValue:@"strName1" forKey:@"1"];
NSLog(@"name = %@", aUser->name);
NSLog(@"_name = %@", aUser->_name);
}
return 0;
}
处理nil值
假如我们在一个类中定义两个属性,一个属性是NSString类型的,一个属性是int类型的。当我们给两个属性赋nil值时,NSString属性是可以被赋nil值的,而int类型的值被赋nil时会引发异常,是由于int类型的属性不能接受nil值所导致的。
也就是说,当程序尝试给某个属性设置nil值时,如果该属性并不能接受nil值,那么程序会自动执行该对象的setNilValueForKey:方法。我们同样可以重写该方法来达到我们想要的效果。例如,接下来我们重写该方法,定义一个int类型的属性age,重写该方法使得如果给age属性赋nil值时,就将age赋值为0。代码:
#import "AUser.h"
@implementation AUser {
int age;
}
- (void) setNilValueForKey:(NSString *)key {
//如果尝试将key为name的属性设置为nil
if ([key isEqualToString:@"age"]) {
//将_name设置为0
age = 0;
} else {
//回调父类的setNilValueForKey:执行默认行为
[super setNilValueForKey:key];
}
}
@end
#import <Foundation/Foundation.h>
#import "AUser.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
AUser *aUser = [[AUser alloc] init];
//使用KVC给age属性传nil
[aUser setValue: nil forKey:@"age"];
NSLog(@"age = %@", [aUser valueForKey:@"age"]);
}
return 0;
}
结果:
key路径
KVC除了可以操作对象的额属性之外,还可以操作对象的“复合属性”。所谓“复合属性”,KVC机制将其称为key路径。例如:AUser类里面包含着一个BUser类型的bUser属性,bUser对象中又包含着b1属相和b2属性,那么KVC可以通过bUser.b1、bUser.b2这种key路径来支持操作AUser对象的bUser属性的b1和b2属性。
根据key路径设置属性值的方法:
- setValue: forKeyPath:根据key路径设置属性值
- valueForKeyPath: 根据key路径获取属性值
代码示例:
AUser:
#import <Foundation/Foundation.h>
#import "BUser.h"
NS_ASSUME_NONNULL_BEGIN
@interface AUser : NSObject {
@package
BUser *bUser;
}
@property (nonatomic, assign) int aNumber;
@end
NS_ASSUME_NONNULL_END
BUser:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface BUser : NSObject
@property (nonatomic, copy) NSString *b1;
@property (nonatomic, copy) NSString *b2;
@end
NS_ASSUME_NONNULL_END
#import <Foundation/Foundation.h>
#import "AUser.h"
#import "BUser.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
AUser *aUser = [[AUser alloc] init];
[aUser setValue:@"12" forKey:@"aNumber"];
[aUser setValue:[[BUser alloc] init] forKey:@"bUser"];
[aUser setValue:@"这是b1" forKeyPath:@"bUser.b1"];
[aUser setValue:@"这是b2" forKeyPath:@"bUser.b2"];
NSLog(@"aNumber: %@", [aUser valueForKey:@"aNumber"]);
NSLog(@"b1: %@", [aUser valueForKeyPath:@"bUser.b1"]);
NSLog(@"b2: %@", [aUser valueForKeyPath:@"bUser.b2"]);
}
return 0;
}
结果:
实际上,通过KVC操作对象的性能比通过getter、setter方法操作的性能更差,使用KVC的优点是编程更加灵活,更适合提炼一些通用性质的代码