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<Key>:

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

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<Key>, <key>, or is<Key>, 如果匹配成功调用方法,返回结果。必要的话进行数据类型转换。

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

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

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

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: '[<ViewController 0x7fd60b728690> 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对应的基本数据类型


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对应的结构体类型


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: '[<ViewController 0x7fd769484b90> 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<Key>: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<NSString *, id> *)keyedValues;

//获取一组key的属性值,然后以NSDictionary形式返回
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)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,运算符跟在@符号之后,格式如下图

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:@"array.@distinctUnionOfObjects.name"];
    NSLog(@"distinctArr:%@",distinctArr);
    
    NSArray *undistinctArr = [self valueForKeyPath:@"array.@unionOfObjects.name"];
    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
)

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 1、介绍: KVC键值编码在iOS中允许开发者通过 Key 直接访问对象的属性,或者给对象的属性或者成员变量赋值,...
    寻形觅影阅读 688评论 0 5
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    朽木自雕也阅读 1,548评论 6 1
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    Fendouzhe阅读 667评论 0 6
  • 全称:Key Value Coding(键值编码) 赋值 取值
    YANGGQ阅读 280评论 0 0
  • KVC简单介绍 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key...
    公子无礼阅读 1,376评论 0 6