KVC:键值编码,全称 key value coding。KVC 是 objective-c 支持的一种允许以字符串形式间接操作对象的属性的灵活的操作方式。
最基本的KVC
最基本的KVC由NSKeyValueCoding 协议提供支持。2个最基本的操作属性的方法(设置和获取属性值):
1. setValue:属性名 forKey: 属性名
:为指定属性设置值;
2. valueForKey:属性名
:获取指定属性的值。
示例程序:
FKUser.h
#import <Foundation/Foundation.h>
@interface FKUser : NSObject
// 使用@property定义3个property
@property (nonatomic, copy) NSString* name;
@property (nonatomic, copy) NSString* pass;
@property (nonatomic, copy) NSDate* birth;
@end
FKUser.m
#import "FKUser.h"
@implementation FKUser
// 为3个property合成setter和getter方法,
@synthesize name;
@synthesize pass;
@synthesize birth;
@end
FKUserTest.m
#import "FKUser.h"
int main(int argc , char * argv[])
{
@autoreleasepool{
// 创建FKUser对象
FKUser* user = [[FKUser alloc] init];
// 使用KVC方式为name属性设置属性值
[user setValue:@"孙悟空" forKey:@"name"];
// 使用KVC方式为pass属性设置属性值
[user setValue:@"1455" forKey:@"pass"];
// 使用KVC方式为birth属性设置属性值
[user setValue:[[NSDate alloc]init] forKey:@"birth"];
// 使用KVC获取FKUser对象的属性
NSLog(@"user的name为:%@" , [user valueForKey:@"name"]);
NSLog(@"user的pass为:%@" , [user valueForKey:@"pass"]);
NSLog(@"user的birth为:%@" , [user valueForKey:@"birth"]);
}
}
KVC的编程方式中,不管是 setValue:属性名 forKey: 属性名
方法还是valueForKey:属性名
,都是通过 NSString 对象来指定被操作属性的,forKey: 标签用于传入属性名。
这里介绍下setValue:属性名 forKey: 属性名
和valueForKey:属性名
方法底层的执行机制:
以 代码setValue:属性名 forKey:@"name";
和代码 valueforKey@"name";
为例。
步骤 | setValue:属性名 forKey: 属性名 | valueForKey:属性名 |
---|---|---|
1 | 程序优先考虑调用”setName:属性值; “,代码通过 setter 方法完成设置 | 程序优先考虑调用name; 方法,代码获取getter方法的返回值 |
2 | 若该类没有setName:方法,KVC 机制搜索该类中名为_name 的成员变量对其赋值(不论它是在类接口部分还是实现部分定义,也不论使用什么访问控制符修饰) | 若该类没有name; 方法,KVC 机制搜索该类中名为_name 的成员变量,返回它的值(不论它是在类接口部分还是实现部分定义,也不论使用什么访问控制符修饰) |
3 | 若该类既没setName:方法也无_name成员变量,KVC 机制会搜索名为 name 的成员变量并对其赋值(不论它是在类接口部分还是实现部分定义,也不论使用什么访问控制符修饰) | 若该类既没name; 方法也无_name成员变量,KVC 机制会搜索名为 name 的成员变量并返回它的值(不论它是在类接口部分还是实现部分定义,也不论使用什么访问控制符修饰) |
4 | 前面3步都无效使,系统会执行该对象的 setValue:forUndefineKey: 方法。(它的作用是引发一个异常,结束程序) | 前面3步都无效使,系统会执行该对象的 valueforUndefineKey: 方法。(它的作用是引发一个异常,结束程序) |
示例程序:
FKDog.h
#import <Foundation/Foundation.h>
@interface FKDog : NSObject
{
@package
NSString* name;
NSString* _name;
}
@end
FKDog.m
#import "FKDog.h"
@implementation FKDog
{
int age;
}
@end
FKDogTest.m
#import "FKDog.h"
int main(int argc , char * argv[])
{
@autoreleasepool{
// 创建FKDog对象
FKDog* dog = [[FKDog alloc] init];
// 使用KVC方式为name属性赋值,KVC的搜索顺序为:
// ①.setName:方法、②._name成员变量、③.name成员变量
[dog setValue:@"旺财" forKey:@"name"];
// 访问name、_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"]);
}
}
编译后输出内容为:
dog->_name:(null)
dog->_name:旺财
dog 的 age:5
不存在的 key 的处理
当使用 KVC 方式操作属性时,这些属性可能并不存在——不存在 setter,getter 方法,也不存在对应的成员变量,KVC 会自动调用setValue:forUndefineKey: 方法或者valueforUndefineKey: 方法。但这2个方法只是引发异常,无进一步的处理。
如果这时我们想要不结束程序或者对异常做个响应的处理该怎么办呢?
只要在相应类的实现部分重写setValue:forUndefineKey: 方法或者valueforUndefineKey: 方法即可。而且,在类接口部分可以不用声明该方法。
示例程序:
FKApple.h
#import <Foundation/Foundation.h>
@interface FKApple : NSObject
@end
FKApple.m
#import "FKApple.h"
@implementation FKApple
- (void) setValue:(id)value forUndefinedKey:(id)key
{
NSLog(@"您尝试设置的key:【%@】并不存在!", key);
NSLog(@"您尝试设置的value为:%@", value);
}
- (void) valueForUndefinedKey:(id)key
{
NSLog(@"您尝试访问的key:【%@】并不存在!", key);
}
@end
FKAppleTest.m
#import "FKApple.h"
int main(int argc , char * argv[])
{
@autoreleasepool{
// 创建FKApple对象
FKApple* app = [[FKApple alloc] init];
[app setValue:@"大苹果"forKey:@"name"];
[app valueForKey:@"name"];
}
}
编译运行后输出内容:
您尝试设置的key:【name】并不存在!
您尝试设置的value为:大苹果
您尝试访问的key:【name】并不存在!
特别说明@objective-c并不存在绝对隐藏的方法
objective-c并不存在绝对隐藏的方法!!
即使一个方法仅仅在类实现部分定义,程序依然可以通过 NSObject 提供的 performSelector:
或者performSelector:withObject:
方法调用到 objective-c 对象的方法。
nil 值的处理
当调用 KVC 方式来设置对象属性时,如果我们尝试为基本类型的属性设置一个 nil,会出现什么样的情况呢?
示例程序:
FKItem.h
#import <Foundation/Foundation.h>
@interface FKItem : NSObject
// 使用@property定义两个属性
@property(nonatomic , copy) NSString* name;
@property(nonatomic , assign) int price;
@end
FKItem.m
#import "FKItem.h"
@implementation FKItem
@synthesize name;
@synthesize price;
/*
//重写的setNilValueForKey:方法,这部分后续解开
- (void)setNilValueForKey: (id)key
{
// 如果尝试将key为price的属性设为nil
if([key isEqualToString:@"price"])
{
// 将该price设置为0
price = 0;
}
else
{
// 回调父类的setNilValueForKey,执行默认行为
[super setNilValueForKey: key];
}
}
*/
@end
FKItemTest.m
#import "FKItem.h"
int main(int argc , 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"]);
}
}
程序运行后,输出:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<FKItem 0x100203610> setNilValueForKey]: could not set nil as the value for the key price.'
这里引发了一个NSInvalidArgumentException异常,由于给 int 类型的属性赋nil 造成的。如果程序尝试为某个属性设置为 nil, 若该属性不接受nil 值,那么程序将会自动执行giant 对象的setNilValueForKey:
方法。我们同样可以重写该方法,在上面的代码中打开注释的重写的setNilValueForKey:
方法部分。
再次编译运行,输出结果:
item的name为:(null)
item的price为:0
key 路径
KVC不仅可以操作对象的属性,还可以操作对象的”复合属性”。
**复合属性:**KVC 机制称之为 key路径。举个栗子:FKOrder对象内包含一个 FKItem 类型的item属性,而 FKItem 对象又包含name,price2个属性,那么 KVC 可以通过 item.name,item.price 这种路径来支持操作FKOrder对象的 name,price 属性。
示例程序:
FKItem.h
#import <Foundation/Foundation.h>
@interface FKItem : NSObject
// 使用@property定义两个属性
@property(nonatomic , copy) NSString* name;
@property(nonatomic , assign) int price;
@end
FKItem.m
#import "FKItem.h"
@implementation FKItem
@synthesize name;
@synthesize price;
@end
FKOrder.h
#import <Foundation/Foundation.h>
#import "FKItem.h"
@interface FKOrder : NSObject
// 使用@property定义两个属性
@property(nonatomic , strong) FKItem* item;
@property(nonatomic , assign) int amount;
-(int) totalPrice;
@end
FKOrder.m
#import "FKOrder.h"
@implementation FKOrder
@synthesize item;
@synthesize amount;
-(int) totalPrice
{
return item.price * amount;
}
@end
FKOrderTest.m
#import "FKOrder.h"
int main(int argc , char * argv[])
{
@autoreleasepool{
// 创建FKItem对象
FKOrder* order = [[FKOrder alloc] init];
// 使用KVC方式为amount设置属性值
[order setValue:@"12" forKey:@"amount"];
[order setValue:[[FKItem alloc] init] forKey:@"item"];
// 使用setValue:forKeyPath设置item属性的name属性
[order setValue:@"鼠标" forKeyPath:@"item.name"];
// 使用valueForKeyPath来获取复合属性值
[order setValue:[NSNumber numberWithInt:20] forKeyPath:@"item.price"];
//注意[order valueForKey:@"totalPrice"]的使用
NSLog(@"订单包含%@个%@,总价为:%@", [order valueForKey:@"amount"] ,
[order valueForKeyPath:@"item.name"],
[order valueForKey:@"totalPrice"]);
}
}
运行后输出结果:
订单包含12个鼠标,总价为:240
KVC方式的优势
虽然性能比 setter,getter 方法的操作性能要差,但使用 KVC 编程更为简洁。更适合提炼一些通用性质的代码。由于 KVC 方式允许通过字符串形式来操作对象的属性,这个字符串既可以是常量也可以是变量,具有极高的灵活性。