天天看点

KVC(键值编码)详解

我之前写过用点语法来设置,修改对象的属性,其实也是通过setter,getter的方法来设置和修改对象的属性。实际上,OC还支持一种更加灵活的操作方式,这种方式允许以字符串的形式间接操作对象的属性。这种方式就是今天我要介绍的KVC.

一、简单的KVC

最简单的KVC的操作属性有两个方法如下:

> setValue:属性值forKey:属性名:为指定属性设置值。

>valueForKey:属性名:获取指定属性的值

例如一下例子:

程序代码:Dog.h

#import <Foundation/Foundation.h>

@interface Dog : NSObject
//使用@property定义3个property(属性)
@property(nonatomic,copy)NSString  * name;
@property(nonatomic,copy)NSString  * pass;
@property(nonatomic,copy)NSString * birth;

@end
           

下面在main函数中使用KVC来设置Dog对象的属性,以及访问Dog对象的属性,程序代码如下:

程序代码:main.m

#import <Foundation/Foundation.h>
#import "Dog.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //创建Dog对象
        Dog *dog = [[Dog alloc]init];
        //使用KVC方式为name设置属性值
        [dog setValue:@"黑马哥" forKey:@"name"];
        //使用KVC方式为pass设置属性值
        [dog setValue:@"222" forKey:@"pass"];
        //使用KVC方式为birth设置属性值
        [dog setValue:@"sdfs" forKey:@"birth"];
        //使用KVC获取dog对象的属性
        NSLog(@"dog的name是:%@",[dog valueForKey:@"name"]);
        NSLog(@"dog的pass是:%@",[dog valueForKey:@"pass"]);
        NSLog(@"dog的birth是:%@",[dog valueForKey:@"birth"]);
    }
    return 0;
}
           
<pre name="code" class="objc">这是使用KVC设置属性的值
           
[dog setValue:@"黑马哥" forKey:@"name"];
           
这是使用KVC获取指定属性的值。
           
<pre name="code" class="objc">NSLog(@"dog的name是:%@",[dog valueForKey:@"name"]);
           

输出为:

2016-02-10 14:18:53.392 点语法访问属性[944:42376] dog的name是:黑马哥
2016-02-10 14:18:53.393 点语法访问属性[944:42376] dog的pass是:222
2016-02-10 14:18:53.393 点语法访问属性[944:42376] dog的birth是:sdfs
Program ended with exit code: 0
           

在KVC变成模式中,无论调用setValue:forKey还是用valueForKey:方法,都是通过NSString对象来指定被操作属性的。

    对于setValue:属性值  [email protected]“name”;代码,底层的执行机制如下:

  1. 首先程序优先考虑调用“setName:”方法,代码通过setter方法完成设置;
  2. 如果没有找到setName方法,KVC机制会搜索该类名为:_name的成员变量
  3. 如果没有_name,那么,KVC机制会继续寻找名为name的成员变量。
  4. 如果上面三条都没有找到,那么系统会执行该对象的setValue:forUndefinedKey:方法。

      《默认的setValue:forUndefinedKey:就是应发一个异常,这个异常常常会导致程序因为异常而结束》

对于[email protected]"name"; 代码,底层的执行机制如下:

  1. 程序优先调用"name;"代码来获取该getter方法的返回值
  2. 如果没有name方法,那么寻找该类名为:_name的成员变量。
  3. 如果没有_name成员变量,就继续寻找,那么久寻找该类名为name的成员变量。
  4. 如果上面的三条都没有达到,那么系统会自动调用该对象的valueforUndefinedKey:方法。

      《默认的valueforUndefinedKey:方法实现就是引发一个异常,这个异常将会导致程序因为异常而结束》 一下程序将对上方阐述的做一个简单的举例: 程序代码 Dog.h

#import <Foundation/Foundation.h>

@interface Dog : NSObject
{
    @package
    NSString * name;
    NSString *_name;
}

@end
           

程序代码 Dog.m

#import "Dog.h"

@implementation Dog
{
    int age;
}

@end
           

程序代码 main.m

#import <Foundation/Foundation.h>
#import "Dog.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       //创建Dog对象
        Dog *dog =[[Dog alloc]init];
        //使用KVC方式为name属性赋值,KVC搜索顺序为:
        //1、setName:方法;2、_name成员变量;3、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"]);
    }
    return 0;
}
           

输出:

2016-02-10 16:00:21.689 点语法访问属性[1021:57028] dog->name:(null)
2016-02-10 16:00:21.690 点语法访问属性[1021:57028] dog->_name:小狗
2016-02-10 16:00:21.691 点语法访问属性[1021:57028] dog的age:5
Program ended with exit code: 0
           

可以从上方输出看出:KVC执行顺序

//1、setName:方法;2、_name成员变量;3、name成员变量
           

当设置name是,执行到_name时,直接识别并输出,但是name并没有输出值,输出的为null。

二、处理不存在的key 当我们用KVC进行操作属性时,这些属性可能并不存在—-即不存在对应得setter,getter方法,也不存在对应的成员变量,KVC将会自动调用setValue:forUndefinedKey:和valueForUndefineKey:方法,但系统默认实现这两个方法仅仅是引发异常,并没有进行特别的处理。例如以下程序:

程序代码:Dog.h

#import <Foundation/Foundation.h>

@interface Dog : NSObject


@end
           

程序代码:main.m

#import <Foundation/Foundation.h>
#import "Dog.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       //创建Dog对象
        Dog *dog =[[Dog alloc]init];
        [dog setValue:@"小狗" forKey:@"name"];
        NSLog(@"dog的age:%@",[dog valueForKey:@"name"]);
    }
    return 0;
}
           

运行将会输出:<下方为输出的异常>

2016-02-10 16:12:58.446 点语法访问属性[1049:59283] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Dog 0x100206710> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.'
           

       上面异常的信息为:程序尝试设置的name并不存在,因此,程序引发了NSUnknownKeyException异常,我们在Dog.m文件中重写方法:         -(void)setValue:(id)value forUndefinedKey:(NSString *)key 代码如下:

-(void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    NSLog(@"您设置的key:%@并不存在",key);
    NSLog(@"您设置的value为:%@",value);
}
           

再次运行程序,可以看到如下输出的程序代码:

2016-02-10 18:06:15.387 点语法访问属性[1179:72930] 您设置的key:name并不存在
2016-02-10 18:06:15.388 点语法访问属性[1179:72930] 您设置的value为:小狗
2016-02-10 18:06:15.389 点语法访问属性[1179:72930] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Dog 0x100114550> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'
           

看上方程序依然引发异常,因为我们还用了valueForKey这个方法,因为不存在name,_name也不存在name方法,KVC机制会调用方法valueForUndefinedKey:,为了实现自定义行为,为了程序不蹦,我们在Dog.m文件中进行以下方法;

-(id)valueForUndefinedKey:(NSString *)key
{
    NSLog(@"您尝试访问的key:%@并不存在",key);
    return nil;
}
           

   此时我们可以看到,当KVC操作并不存在的key时,KVC机制总是调用重写过得方法进行处理,通过这种处理机制,可以非常方便的定制自己的处理行为。

三、处理nil值

当我们调用KVC来设置对象的属性时,如果属性的类型是基本数据类型(如:int,float,double),且程序传入了对应类型的值,当用KVC设置值时会报错 程序模拟如下:

程序代码:Dog.h

#import <Foundation/Foundation.h>

@interface Dog : NSObject
//使用@property定义两个属性
@property(nonatomic,copy)NSString *name;
@property(nonatomic,assign)int price;

@end
           

程序代码:main.m

#import <Foundation/Foundation.h>
#import "Dog.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       //创建Dog对象
        Dog *dog =[[Dog alloc]init];
        //使用KVC方式尝试为name,price设置属性为nil
        [dog setValue:nil forKey:@"name"];
        [dog setValue:nil forKey:@"price"];
        NSLog(@"dog的name为%@",[dog valueForKey:@"name"]);
        NSLog(@"dog的price为%@",[dog valueForKey:@"price"]);
    }
    return 0;
}
           

运行以上程序会报错,报错信息为:

2016-02-10 18:28:50.763 点语法访问属性[1294:79054] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<Dog 0x100203d40> setNilValueForKey]: could not set nil as the value for the key price.'
           

NSInvalidArgumentException

从上方程序可以看出,程序引发了一个‘NSInvalidArgumentException’异常,这就是由于int类型的属性不能接受nil所导致的,上面这段提示信息其实是有程序中的setNilValueForKey:方法所产生的。也就是说,当程序尝试为某个属性设置nil时,如果该属性并不接受nil值,那么程序将会自动执行该对象的setNilValueForKey:方法。如果为了定制这个行为,我们可以重写这个方法。在Dog的类的实现部分增加以下代码就可以

-(void)setNilValueForKey:(NSString *)key
{
    //如果尝试将key为price的属性设为nil
    if([key isEqualToString:@"price"])
    {
        _price = 0;
    }
    else
    {
        //回调父类的setNilValueForKey,执行默认行为
        [super setNilValueForKey:key];
    }
}
           

输出为:

2016-02-10 18:37:20.098 点语法访问属性[1328:81680] dog的name为(null)
2016-02-10 18:37:20.100 点语法访问属性[1328:81680] dog的price为0
           

四、Key路径 kvc除了操作对象之外,还可操作对象的”复合属性“,所谓复合属性,就是Key路径,比如:Item对象里面包含一个Dog类型的dog属性,而dog对象又包含了name和price 属性,那么kvc可以通过dog.name、dog.price这种Key路径来支持操作Item对象的dog属性的name 、price属性。 KVC协议中有如下两种方法操作Key路径 >setValue:forKeyPath: 根据Key路径设置属性值 >valueForKeyPath:根据Key路径获取属性值

程序代码:Dog.h

#import <Foundation/Foundation.h>

@interface Dog : NSObject
//使用@property定义两个属性
@property(nonatomic,copy)NSString *name;
@property(nonatomic,assign)int price;

@end
           

程序代码:Item.h

//
//  Item.h

//  Created by lyx on 16/2/10.
//  Copyright (c) 2016年 李云祥. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "Dog.h"

@interface Item : NSObject
@property(nonatomic,strong)Dog *dog;
@property(nonatomic,assign)int amount;
@property(nonatomic,assign)int aollPrice;
@end
           

程序代码:main.m

#import <Foundation/Foundation.h>
#import "Item.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //创建Item对象
        Item *item = [[Item alloc]init];
        //使用KVC方式为amount赋值
        [item setValue:@"287" forKey:@"amount"];
        [item setValue:[[Dog alloc]init] forKey:@"dog"];
        //使用setValue:forKeyPath设置dog属性的name属性
        [item setValue:@"键盘" forKeyPath:@"dog.name"];
        [item setValue:@"1189" forKeyPath:@"dog.price"];
        
        NSLog(@"%@个%@,总的为:%@",[item valueForKey:@"amount"],[item valueForKeyPath:@"dog.name"],[item valueForKey:@"aollPrice"]);
    
    }
    return 0;
}
           

下方是输出的值:

2016-02-10 19:42:58.143 点语法访问属性[1508:93608] 287个键盘,总的为:0
Program ended with exit code: 0
           

通过KVC操作对象的性能比通过setter,getter方法操作的性能更差,使用KVC变成的优势是更加简洁,更适合提炼一些通用性质的代码,由于KVC方式允许通过字符串的方式来操作对象的属性,这个字符串既可以是常量,也可以使变量,因此,具有极高的灵活性。