KVC
KVC 是 Objective-C 中的键值编码(Key-Value Coding)的缩写。它是一种通过键(Key)来访问对象的属性值的机制,而不是直接使用属性的访问器方法或实例变量。
使用 KVC,你可以通过字符串键来获取、设置或观察对象的属性值,而不需要知道属性的具体名称或实现细节。这为动态访问和操作对象的属性提供了一种便捷的方式。
最基本的KVC是由NSKeyValueCoding协议提供支持,有下面两个最基本操作属性的方法:
- setValue:属性值 forKey:属性名: 为指定属性设置属性值
- valueForKey:属性名 :获取指定属性的值
1.简单的KVC
先看下面代码:
首先定义一个FKUser的类:往类中定义3个属性。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKUser : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, copy) NSString* pass;
@property (nonatomic, copy) NSDate* birth;
@end
NS_ASSUME_NONNULL_END
然后在实现部分通过 @synthesize对这3个属性合成getter和setter方法。
接下来通过KVC来设置FKUser对象的属性,以及访问属性。代码如下:
#import <Foundation/Foundation.h>
#import "FKUser.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
FKUser* user = [[FKUser alloc] init];
[user setValue:@"孙悟空" forKey:@"name"];
[user setValue:@"12345" forKey:@"pass"];
[user setValue: [[NSDate alloc] init] forKey:@"birth"];
NSLog(@"user的name为: %@", [user valueForKey:@"name"]);
NSLog(@"user的pass为: %@", [user valueForKey:@"pass"]);
NSLog(@"user的birth为: %@", [user valueForKey:@"birth"]);
}
return 0;
}
运行结果如下:
在KVC编程方式中,无论调用 setValue:forKey 方法,还是调用 valueForKey:方法,都是通过 NSString 对象来指定被操作属性的,其中 forKey:标签用于传入属性名。
对于 setValue:属性值 forKey@“name”;代码,底层的执行机制如下。
- (1)程序优先考虑调用“setName:属性值;”代码通过 setter方法完成设置。
- (2)如果该类没有 setName:方法, KVC 机制会搜索该类名为_name的成员变量,无论该成员变量是在类接口部分定义,还是在类实现部分定义,也无论用哪个访问控制符修饰,这条KVC代码底层实际上就是对_name成员变量赋值。
- (3)如果该类既没有 setName:方法,也没有定义_name成员变量, KVC机制会搜索该类名为 name的成员变量,无论该成员变量是在类接口部分定义,还是在类实现部分定义,也无论用哪个访问控制符修饰,这条KVC代码底层实际就是对 name成员变量赋值。
- (4)如果上面 3条都没有找到,系统将会执行该对象的 setValue: forUndefinedKey:方法。
对于valueforKey@“name”;代码底层的执行逻辑和上面差不多。
这边通过代码示例:
先定义一个FKDog的类
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKDog : NSObject {
@package
NSString* name;
NSString* _name;
}
@end
NS_ASSUME_NONNULL_END
#import "FKDog.h"
@implementation FKDog {
int age;
}
@end
在主函数中测试程序:
#import <Foundation/Foundation.h>
#import "FKUser.h"
#import "FKDog.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
//创建FKDog对象
FKDog* dog = [[FKDog alloc] init];
//使用KVC方式为name属性赋值,
[dog setValue:@"旺财" forKey:@"name"];
NSLog(@"dog->name:%@", dog->name);
NSLog(@"dog->name:%@", dog->_name);
//使用KVC对age赋值,将会导致实现部分的age成员变量被赋值
[dog setValue:[NSNumber numberWithInt:5] forKey:@"age"];
NSLog(@"dog->age:%@", [dog valueForKey:@"age"]);
}
return 0;
}
代码运行的结果如下:
因此可以总结出:
KVC的搜索顺序为:
- setName:方法
- _name成员变量
- name成员变量
2.处理不存在的key
当使用KVC方法操作属性的时候,这些属性可能并不存在——既不存在对应的setter,getter方法。也不存在对应的成员变量,KVC将会自动调用setValue:forUndefineKey:和valueForUndefineKey:方法系统默认实现的这两个方法仅仅只是引发异常,并不进行任何处理。例如下面程序定义了一个FKApple类,该类没有定义的成员变量和方法。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKApple : NSObject
@end
NS_ASSUME_NONNULL_END
int main(int argc, const char * argv[]) {
@autoreleasepool {
FKApple* apple = [[FKApple alloc] init];
[apple setValue:@"大苹果" forKey:@"name"];
[apple valueForKey:@"name"];
}
return 0;
}
运行结果如下:
上面异常信息提示:程序尝试设置的name并不存在,因此,程序引发了NSUnknownKeyException异常。
对于这种情况,可以选择在FKApple.m重写 setValue:forUndefined Key: 方法,并且再加入 - (void)valueForUndefinedKey:(id)key 方法。
#import "FKApple.h"
@implementation FKApple
- (void) setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"你尝试访问的key:【%@】并不存在!", key);
NSLog(@"你尝试设置的value:%@", value);
}
- (void)valueForUndefinedKey:(id)key {
NSLog(@"你访问的key:【%@】不存在",key);
}
@end
代码运行结果如下:
3.处理nil值
当调用KVC来设置对象的属性的时候,属性的类型为基本类型(int,float,double),如果为基本类型的属性设置一个nil,会出现什么效果呢?
接下来通过下面的代码来实现:
先定义一个FKItem的类:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKItem : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) int price;
@end
NS_ASSUME_NONNULL_END
在主函数中给他们赋值为空。
#import "FKItem.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
//创建FKItem对象
FKItem* item = [[FKItem alloc] init];
//使用KVC方法尝试为name,price属性设置为nil。
[item setValue:nil forKey:@"name"];
[item setValue:nil forKey:@"price"];
NSLog(@"item的name为:%@", [item valueForKey:@"name"]);
NSLog(@"item的price为:%@", [item valueForKey:@"price"]);
}
return 0;
}
代码运行结果如下:
由于int类型的price属性不接受nil的值所导致的结果。如果该属性不接受nil值那么程序将会自动执行该对象setNilValueForKey:方法。
如果为了自行定义这种行为,可以通过重写setNilValueForKey:方法来实现。下面是重写setNilValueForKey:方法。
#import "FKItem.h"
@implementation FKItem
- (void) setNilValueForKey:(NSString *)key {
//如果尝试将key为price的属性设置为nil。
if ([key isEqualToString:@"price"]) {
//将该price设置为0
_price = 0;
} else {
//回调父类的setNilValueForKey,执行默认行为
[super setNilValueForKey:key];
}
}
@end
重新运行后的结果:
通过重写 setNilValueForKey: 方法,你可以在给属性赋值为 nil 时提供自定义的行为,以避免程序崩溃或产生不可预料的结果。这在处理可能为 nil 的属性时特别有用。
4.Key路径
KVC处理操作对象的属性之外,还可以操作对象的属性的“复合属性”。
“复合属性”,KVC机制将其称为Key路径。例如,FKOrder对象中包含一个FKItem类型的item属性,而FKItem对象又包含了name属性以及price属性,因此KVC可以通过item.name和item.pricez这种路径来支持操作FKOrder对象的name,price属性。
KVC协议中为操作Key路径的方法如下:
- setValue: forKeyPath: :根据Key的路径设置属性值。
- valueForKeyPath: :根据Key的路径获取属性值。
通过下面代码来实现:
先定义一个FKOrder的类。
#import <Foundation/Foundation.h>
#import "FKItem.h"
NS_ASSUME_NONNULL_BEGIN
@interface FKOrder : NSObject
@property (nonatomic, strong) FKItem* item;
@property (nonatomic, assign) int amount;
@end
NS_ASSUME_NONNULL_END
#import "FKOrder.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
FKOrder* order = [[FKOrder alloc] init];
//使用KVC方法为amount设置属性值
[order setValue:@"12" forKey:@"amount"];
[order setValue:[[FKItem alloc] init] forKey:@"item"];
//使用setValue:forKeyPath来获取复合值
[order setValue:@"鼠标" forKeyPath:@"item.name"];
[order setValue:[NSNumber numberWithInt:20] forKeyPath:@"item.price"];
NSLog(@"订单包含%@个%@,单价为%@" ,[order valueForKey:@"amount"], [order valueForKeyPath:@"item.name" ],[order valueForKeyPath:@"item.price"]);
}
return 0;
}
代码运行结果如下:
实际上,通过 KVC操作对象的性能比通过 setter、getter 方法操作的性能更差,使用KVC编程的优势更加简洁,更适合提炼一些通用性质的代码。由于KVC方式允许通过字符串形式来操作对象的属性,这个字符串既可是常量,也可是变量,因此具有极高的灵活性。