Key-Value Coding(键值编码)

一、KVC简介

KVC提供了一套不通过访问器方法或者属性变量,通过Key或者KeyPath直接访问对象属性的机制。KVC是以下技术的实现基础KVO、Core Data、Cocoa bindings、AppleScript。KVC性能略逊于访问器和实例变量,但是灵活性高,很多时候可以简化代码。使用KVC需要实现其存取方法,相关的方法都在Objective-C的NSKeyValueCoding协议中声明,超级父类NSObject默认遵守该协议。KVC支持对象属性(如NSSting)同时也指出非对象属性(基本数据类型和结构体,提供自动转换数据类型)。

二、KVC基本原理

首先区分两个基本概念

名称 内容
Key Key是标识对象具体属性的字符串,相当于对象的访问器名称或者变量名称,不能包含空格。
KeyPath KeyPath是指定对象一系列属性,且用.分割每个属性的字符串。字符串序列中的每个key标识前面对象的属性。比如说people.address.street能够获取people的address属性,然后获取到address的street属性。

然后说明等的执行过程,KVC的方法从功能上分存、取两种方法setValue:forKey:valueForKey:,以这两个方法为代表描述执行过程。

首先setValue:forKey:的执行过程
1、首先对象方法列表中匹配方法-set:

2、如果第1步失败而且 accessInstanceVariablesDirectly 返回YES,按照以下顺序匹配实例变量_, _is, , or is

3、如果前2步任一成功,则进行赋值。必要的话进行数据类型转换。

4、如果前3步进行失败则调用 setValue:forUndefinedKey: 抛出NSUndefinedKeyException异常。

注:方法setValue:forKey:根据指定路径获取属性值,KeyPath中每一个key都进行以上步骤;也就是说任何一个key出错,都会抛出异常。

代码2.1
@interface ViewController ()
{
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;

}
@property (nonatomic,copy)NSString *name;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setValue:@"zwq" forKey:@"name"];
    
    NSLog(@"_name:%@",_name);
    NSLog(@"_isName:%@",_isName);
    NSLog(@"name:%@",name);
    NSLog(@"isName:%@",isName);
    }
//可以通过以上代码(注释部分代码)来验证上述过程。    

然后是valueForKey:执行过程

1、首先按照此顺序匹配方法 get, , or is, 如果匹配成功调用方法,返回结果。必要的话进行数据类型转换。

2、如果1步进行失败,则匹配以下方法 countOf、 objectInAtIndex: 、 AtIndexes:若找打其中一个,则返回容器类对象。该对象调用以上方法,会调用valueForKey:方法。(NSArray类的方法)

3、如果前2步失败,则匹配以下方法countOf, enumeratorOf, and memberOf:若找打其中一个,则返回容器类对象。该对象调用以上方法,会调用valueForKey:方法。
(NSSet类的方法)

4、如果前3步失败,而且 accessInstanceVariablesDirectly 返回YES,按照以下顺序匹配实例变量_, _is, , or is。如果实例变量找到了,则进行复制。必要的话进行数据类型转换。

5、如果前4步进行失败则调用 valueForUndefinedKey: 抛出NSUndefinedKeyException异常。

注:
1、方法valueForKeyPath:根据指定路径获取属性值,KeyPath中每一个key都进行以上步骤;也就是说任何一个key出错,都会抛出异常。
2、如果KeyPath序列中包含了一个key是一对多的关系,而且这个key不是最后一个,那么将返回所有对象的属性值。例如accounts.transactions.payee将返回所有account的所有transaction的所有payee值。

//VC有一个数组属性
@property (nonatomic,assign)NSArray *array;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //Data有一个name属性
    Data *data1 = [[Data alloc] init];
    Data *data2 = [[Data alloc] init];
    Data *data3 = [[Data alloc] init];
    data1.name=@"data1";
    data2.name=@"data2";
    data3.name=@"data3";
    
    //self.array.name
    NSArray *arr = [NSArray arrayWithObjects:data1,data2,data3, nil];
    [self setValue:arr forKey:@"array"];
    NSLog(@"array:%@",[self valueForKeyPath:@"array.name"]);
    }
    
输出结果
2016-09-01 17:05:57.235 KVC[3467:249694] array:(
    data1,
    data2,
    data3
)

可以仿照代码2.1进行代码验证。由上边底层执行过程不难看出:KVC性能略逊于访问器和实例变量,但是灵活性高,视情况选择。

说明:

1、必要的话进行数据类型转换:KVC对应非对象类型进行自动数据类型转换,下文做详细说明。
2、方法accessInstanceVariablesDirectly的说明:默认返回YES,表示对象的实例变量可以直接访问。
3、关于NSUndefinedKeyException异常的处理,下文做详细说明

三、异常处理

1、方法valueForKey:寻找不到指定Key或者KeyPath匹配的方法或变量名称会自动调用valueForUndefinedKey: 抛出NSUndefinedKeyException异常
2、方法setValue:forKey:寻找不到指定Key或者KeyPath匹配的方法或变量名称会自动调用setValue:forUndefinedKey: 抛出NSUndefinedKeyException异常

//NSUndefinedKeyException如下所示
 *** Terminating app due to uncaught exception 'NSUnknownKeyException', 
 reason: '[ setValue:forUndefinedKey:]: 
 this class is not key value coding-compliant for the key age.'

处理方法为重写此二者方法

- (nullable id)valueForUndefinedKey:(NSString *)key;

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

方法体可为空也可自定义处理

//空处理
- (nullable id)valueForUndefinedKey:(NSString *)key
{
    return nil;
}
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key
{

}

//自定义处理
- (nullable id)valueForUndefinedKey:(NSString *)key
{
    if ([key isEqualToString:@"key"]) {
        //返回内容自定义
        return nil;
    }
    return nil;
}
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key
{
    if ([key isEqualToString:@"key"])
    {
        //返回内容自定义
    }
}

四、非对象类型的处理

KVC对于基本数据类型和结构体在底层支持自动数据类型转换。根据相对的存取方法或者实例变量判端实际需要的值类型,选择NSNumber 或 NSValue 进行自动转换。
1、NSNumber对应的基本数据类型


Key-Value Coding(键值编码)_第1张图片
14726339516105.jpg

例如

@property (nonatomic,assign)BOOL fail;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSNumber *num = [NSNumber numberWithBool:0];
    NSLog(@"class:%@",[num class]);
    
    [self setValue:@"0" forKey:@"fail"];
    NSLog(@"fali:%d--class:%@",self.fail,[[self valueForKey:@"fail"] class]);
    }
 
 输出结果:
 2016-09-01 14:27:33.401 KVC[2672:154097] class:__NSCFBoolean
 2016-09-01 14:27:33.401 KVC[2672:154097] fali:0--class:__NSCFBoolean   

2、NSValue对应的结构体类型


Key-Value Coding(键值编码)_第2张图片
14726339706102.jpg

例如

@property (nonatomic,assign)CGPoint point;

    NSValue *value = [NSValue valueWithCGPoint:CGPointMake(1, 1)];
    NSLog(@"class:%@",[value class]);

    [self setValue:value forKey:@"point"];
    NSLog(@"fali:%@--class:%@",NSStringFromCGPoint(self.point) ,[[self valueForKey:@"point"] class]);
    
输出结果:
2016-09-01 14:40:23.599 KVC[2751:163036] class:NSConcreteValue
2016-09-01 14:40:23.599 KVC[2751:163036] fali:{1, 1}--class:NSConcreteValue

3、注意事项
对非对象类型的属性设置nil空值,底层调用setNilValueForKey:,然后抛出NSInvalidArgumentException异常
例如

 [self setValue:nil forKey:@"fail"];
 //或
 [self setValue:nil forKey:@"point"];
 
 异常:
 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
 reason: '[ setNilValueForKey]: 
 could not set nil as the value for the key fail.'
 

解决方法是重写该方法setNilValueForKey:,方法可空也可自定义处理,例如

-(void)setNilValueForKey:(NSString *)key
{
    //自定义内容
    if ([key isEqualToString:@"fail"])
    {
        [self setValue:[NSNumber numberWithBool:0] forKey:@"fail"];
    }
    if ([key isEqualToString:@"point"])
    {
        [self setValue:[NSValue valueWithCGPoint:CGPointZero] forKey:@"point"];
    }
}

五、Key-Value Validation

这个标题就不翻译了,英文更容易理解。

- validateValue:forKey:error:
- validateValue:forKeyPath:error:

KVC提供一套API使得属性值生效。使得对象有机会接受值、提供默认值、拒绝新值、抛出错误原因。KVC不会自动调用,需要手动调用。默认实现过程:
1、调用validateValue:forKey:error:
2、在对象的方法列表中匹配validate:error:
3、如果找到则执行并返回结果
4、如果未找到则返回YES,并赋值
注意:set方法中禁止调用

@property (nonatomic,assign)NSInteger age;

-(BOOL)validateAge:(id *)ioValue error:(NSError **)outError
{
    
    if (*ioValue == nil)
    {
        // 年龄大于0岁
        [self setValue:@"0" forKey:@"age"];
        return YES;
    }
    if ([*ioValue floatValue] <= 0.0)
    {
        if (outError != NULL)
        {
            NSString *errorString = NSLocalizedStringFromTable(
                                                               @"年龄要大于0岁", @"人",
                                                               @"年龄错误");
            NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorString };
            NSError *error = [[NSError alloc] initWithDomain:@"年龄校验"
                                                        code:0
                                                    userInfo:userInfoDict];
            *outError = error;
        }
        return NO;
    }
    else
    {
        return YES;
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSNumber *ageNum = [NSNumber numberWithInteger:0];
    NSError *error = nil;
    [self validateValue:&ageNum forKey:@"age" error:&error];
    NSLog(@"error:%@",error);
    }
    
输出结果
2016-09-01 15:30:29.661 KVC[3044:197432] error:Error Domain=年龄校验 Code=0 "年龄要大于0岁" UserInfo={NSLocalizedDescription=年龄要大于0岁}

五、容器类

关于KVC在容器类中的应用。容器类主要包括:NSDictionary、NSArray、NSSet三种。关于容器类的操作方法有很多,分类整理一下
1、如果作为对象的一个属性值,那就作为对象属性处理,无论Key还是KeyPath都符合前四条中说的规则;
2、就可变不可变来说,一般来说存什么取什么,但是可以根据需要获取相应的方法

@property (nonatomic,assign)NSMutableArray *mutableArray;

@property (nonatomic,assign)NSArray *array;

- (void)viewDidLoad {
    [super viewDidLoad];
        [self setValue:[NSArray arrayWithObjects:@"zwq", nil] forKey:@"array"];
    [self setValue:[NSMutableArray arrayWithObjects:@"zwq2", nil] forKey:@"mutableArray"];
    NSLog(@"不可变:%@--%@",[[self valueForKey:@"array"] class],[[self mutableArrayValueForKey:@"array"] class]);
    NSLog(@"可变:%@--%@",[[self valueForKey:@"mutableArray"] class],[[self mutableArrayValueForKey:@"mutableArray"] class]);
    }
    
输出结果
2016-09-01 16:30:55.057 KVC[3328:231529] 不可变:__NSArrayI--NSKeyValueSlowMutableArray
2016-09-01 16:30:55.057 KVC[3328:231529] 可变:__NSArrayM--NSKeyValueSlowMutableArray

//KeyPath道理也是一样的

3、需要单独说的是NSDictionary跟NSArray有点不一样,而且功常用一点

//根据指定dic设置对象属性值。使用dic的key来标识属性,dic的value标识值,底层调用setValue:forKey:进行赋值。
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

//获取一组key的属性值,然后以NSDictionary形式返回
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

一个常见的功能应用,获取网络数据,数据解析完毕然后赋值的时候,如果Key很多是个很麻烦的事情,但是使用setValuesForKeysWithDictionary:一行代码搞定

//比如Model的属性
@property (nonatomic,copy)NSString *name;
@property (nonatomic,copy)NSString *address;

- (void)viewDidLoad {
    [super viewDidLoad];
    //比如需要解析的数据
    NSDictionary *dic =@{@"name":@"zwq",@"address":@"地球"};
    [self setValuesForKeysWithDictionary:dic];
    NSLog(@"name:%@--address:%@",self.name,self.address);
    }
    
    输出结果
    2016-09-01 16:42:47.898 KVC[3367:237574] name:zwq--address:地球

注意:
1、如果dic中有未定义的key那么需要进行异常处理,参考《三、异常处理》段落。
2、容器类比如NSArray, NSSet, NSDictionary不能包含nil值,需要使用NSNull替换(一个表示nil值的单例类)
3、方法dictionaryWithValuesForKeys:和setValuesForKeysWithDictionary:会自动转换NSNull和nil,不需要过多关注。

4、容器类运算符
容器类运算是valueForKeyPath:中特殊的KeyPath,运算符跟在@符号之后,格式如下图

Key-Value Coding(键值编码)_第3张图片
Paste_Image.png

整个KeyPath以运算符为中心,分为3部分。左边的路径标识容器类(set或者array)的访问路径,中间是运算符,右边是参加运算的属性访问路径。

暂不支持自定义运算符,总体分为三种;

分类 内容
基本运算符 @avg(平均值)、@count(数量)、@max(最大值)、 @min(最小值)、@sum(求和)
对象运算符 @distinctUnionOfObjects(祛同属性值集合)、@unionOfObjects(属性值集合)
容器运算符 @distinctUnionOfArrays()、@unionOfArrays()、@distinctUnionOfSets()

选择其中一个演示一下,其它的运算符同理。

//VC有一个数组属性
@property (nonatomic,assign)NSArray *array;


- (void)viewDidLoad {
    [super viewDidLoad];
    
    //Data有一个name属性
    Data *data1 = [[Data alloc] init];
    Data *data2 = [[Data alloc] init];
    Data *data3 = [[Data alloc] init];
    data1.name=@"data1";
    data2.name=@"data2";
    data3.name=@"data3";
    
    //self.array.name
        NSArray *arr = [NSArray arrayWithObjects:data1,data2,data1, nil];
    [self setValue:arr forKey:@"array"];

    NSArray *distinctArr = [self valueForKeyPath:@"[email protected]"];
    NSLog(@"distinctArr:%@",distinctArr);
    
    NSArray *undistinctArr = [self valueForKeyPath:@"[email protected]"];
    NSLog(@"undistinctArr:%@",undistinctArr);
    }
    
输出结果
2016-09-01 17:17:59.049 KVC[3507:256556] distinctArr:(
    data1,
    data2
)
2016-09-01 17:17:59.050 KVC[3507:256556] undistinctArr:(
    data1,
    data2,
    data1
)

以上问本人自己学习感悟,理解并整理。更多内容请查看官方文档。

你可能感兴趣的:(Key-Value Coding(键值编码))