iOS KVC

一、简介

KVC 的全称是 Key-Value Coding(键值编码),是由 NSKeyValueCoding 非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问,可以通过字符串来访问一个对象的成员变量或其关联的存取方法(getter or setter)。

通常,我们可以直接通过存取方法或变量名来访问对象的属性。我们也可以使用 KVC 间接访问对象的属性,并且 KVC 还可以访问私有变量。某些情况下 KVC 还可以帮助简化代码。

二、使用

1.访问对象属性
(1)常用 API
- (nullable id)valueForKey:(NSString *)key;         // 通过 key 来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通过 keyPath 来取值

- (void)setValue:(nullable id)value forKey:(NSString *)key;         // 通过 key 来赋值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通过 keyPath 来赋值
(2)基础操作
#import <Foundation/Foundation.h>
#import "DJTestModelOne.h"
@interface DJTestModel : NSObject

@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *test;
@property (nonatomic,strong) DJTestModelOne *one;

@end
#import <Foundation/Foundation.h>

@interface DJTestModelOne : NSObject
@property (nonatomic,copy)NSString *oneName;
@end
#import "ViewController.h"
#import "DJTestModel.h"
#import "DJTestModelOne.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    
    DJTestModel *model = [[DJTestModel alloc]init];

    //直接赋值
    [model setName:@"dj"];
    
    //使用 KVC 赋值
    [model setValue:@"dj" forKey:@"name"];
    
    DJTestModelOne *oneModel = [[DJTestModelOne alloc]init];
    model.one = oneModel;
    
    //使用 KVC 的 keyPath 赋值
    [model setValue:@"djOne" forKeyPath:@"one.oneName"];
    
    //使用 KVC 取值
    NSString *name = [model valueForKey:@"name"];
    
    //使用 KVC 的 keyPath 取值
    NSString *oneName = [model valueForKeyPath:@"one.oneName"];
    
    
}

@end

KVC 还支持多级访问,KeyPath 用法跟点语法相同。

(3)多值操作

给定一组 Key,获得一组 value,以字典的形式返回。该方法为数组中的每个 Key 调用 valueForKey: 方法。

[model setValuesForKeysWithDictionary:@{@"name":@"djValue",@"test":@"testValue"}];

将指定字典中的值设置到消息接收者的属性中,使用字典的 Key 标识属性。默认实现是为每个键值对调用 setValue:forKey: 方法 ,会根据需要用 nil 替换 NSNull 对象。

NSDictionary *dict = [model dictionaryWithValuesForKeys:@[@"name",@"test"]];
2.访问集合属性

我们可以像访问其它对象一样使用 valueForKey:setValue:forKey: 方法来获取或设置集合对象(主要指 NSArrayNSSet)。但是,当我们要操作集合对象的内容,比如添加或者删除元素时,通过 KVC 的可变代理方法获取集合代理对象是最有效的。
【扩展:根据 KVO 的实现原理,是在运行时动态生成子类并重写 setter 方法来达到可以通知所有观察者对象的目的,因此我们对集合对象进行操作是不会触发 KVO 的。当我们要使用 KVO 监听集合对象变化时,需要通过 KVC 的可变代理方法获取集合代理对象,然后对代理对象进行操作。当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。】

KVC 提供了三种不同的代理对象访问的代理方法,每种都有 Key 和 KeyPath 两种方法。

  • mutableArrayValueForKey:mutableArrayValueForKeyPath: 返回 NSMutableArray 对象的代理对象。
  • mutableSetValueForKey:mutableSetValueForKeyPath: 返回 NSMutableSet 对象的代理对象。
  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath: 返回 NSMutableOrderedSet 对象的代理对象。
3.使用集合运算符

KVC 的 valueForKeyPath: 方法除了可以取出属性值以外,还可以在 KeyPath 中嵌套集合运算符,来对集合对象进行操作。

以下是KeyPath集合运算符的格式,主要分为 3 个部分:

  • Left key path:左键路径,要操作的集合对象,如果消息接收者就是集合对象,则可以省略 Left 部分;
  • Collection operator:集合运算符;
  • Right key path:右键路径,要进行运算的集合中的属性。

集合运算符主要分为三类:

  • 聚合运算符:以某种方式合并集合中的对象,并返回右键路径中指定的属性的数据类型匹配的一个对象,一般返回 NSNumber 实例。
  • 数组运算符:根据运算符的条件,将符合条件的对象以一个 NSArray 实例返回。
  • 嵌套运算符:处理集合对象中嵌套其他集合对象的情况,并根据运算符返回一个 NSArrayNSSet 实例。

示例:

#import <Foundation/Foundation.h>
#import "DJTestModelOne.h"
@interface DJTestModel : NSObject
@property (nonatomic,strong) NSArray <DJTestModelOne *> *oneArray;
@end
#import <Foundation/Foundation.h>

@interface DJTestModelOne : NSObject
@property (nonatomic,assign)NSInteger num;
@property (nonatomic,copy)NSString * type;
@end
(1)使用聚合运算符
#import "ViewController.h"
#import "DJTestModel.h"
#import "DJTestModelOne.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    
    DJTestModel *model = [[DJTestModel alloc]init];

    DJTestModelOne *one1 = [[DJTestModelOne alloc]init];
    one1.num = 1;
    
    DJTestModelOne *one2 = [[DJTestModelOne alloc]init];
    one2.num = 2;
    
    DJTestModelOne *one3 = [[DJTestModelOne alloc]init];
    one3.num = 3;
    
    DJTestModelOne *one4 = [[DJTestModelOne alloc]init];
    one4.num = 4;
    
    model.oneArray = @[one1,one2,one3,one4];
    
    NSNumber *avg = [model.oneArray valueForKeyPath:@"@avg.num"];
    
    NSNumber *conunt = [model.oneArray valueForKeyPath:@"@count"];
    
    NSNumber *sum = [model.oneArray valueForKeyPath:@"@sum.num"];
    
    NSNumber *max = [model.oneArray valueForKeyPath:@"@max.num"];
    NSNumber *min = [model.oneArray valueForKeyPath:@"@min.num"];
    
}

@end
(2)使用数组运算符
#import "ViewController.h"
#import "DJTestModel.h"
#import "DJTestModelOne.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    
    DJTestModel *model = [[DJTestModel alloc]init];

    DJTestModelOne *one1 = [[DJTestModelOne alloc]init];
    one1.type = @"one1";
    
    DJTestModelOne *one2 = [[DJTestModelOne alloc]init];
    one2.type = @"one2";
    
    DJTestModelOne *one3 = [[DJTestModelOne alloc]init];
    one3.type = @"one2";
    
    DJTestModelOne *one4 = [[DJTestModelOne alloc]init];
    one4.type = @"one4";
    
    model.oneArray = @[one1,one2,one3,one4];
    
    //获取集合中的所有 type 对象
    NSArray *types = [model.oneArray valueForKeyPath:@"@unionOfObjects.type"];
    
    // 获取集合中的所有不同的 type 对象
    NSArray *disTypes = [model.oneArray valueForKeyPath:@"@distinctUnionOfObjects.type"];
    
}

在使用数组运算符时,如果有任何操作的对象为 nil,则 valueForKeyPath: 方法将引发异常。

(3)使用嵌套运算符

处理集合对象中嵌套其他集合对象的情况,并根据运算符返回一个NSArray或NSSet实例。

NSArray *arrayOfArrays = @[model.oneArray,model.oneArray];
    
    NSArray *arrayTypes = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.type"];
    
    NSArray *distinctTypes = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.type"];
  • 在使用嵌套运算符时,valueForKeyPath: 内部会根据运算符创建一个 NSMutableArrayNSMutableSet 对象,将集合中的 array 和 set 添加进去再进行操作。如果集合中有非集合元素,会导致 Crash。
  • 使用 unionOfArraysdistinctUnionOfArrays 运算符,消息接收者应该是 arrayOfArrays 类型,即 NSArray< NSArray* >* arrayOfArrays; 使用 distinctUnionOfSets 运算符,消息接收者应该是 setOfSets 或者 arrayOfSets 类型。否则会发生异常。
  • 在使用嵌套运算符时,如果有任何操作的对象为 nil, 则 valueForKeyPath: 方法将引发异常。
(4)拓展

如果集合中的对象都是 NSNumber,右键路径可以用 self

    NSArray *array = @[@1, @2, @3, @4, @5];
    NSNumber *sum = [array valueForKeyPath:@"@sum.self"];
    NSLog(@"%d",[sum intValue]); 
4.非对象值处理

KVC 支持基础数据类型和结构体,在使用 KVC 进行赋值或取值的时候,会自动在非对象值和对象值之间进行转换。

  • 当进行取值如 valueForKey: 时,如果返回值非对象,会使用该值初始化一个 NSNumber(用于基础数据类型)或 NSValue(用于结构体)实例,然后返回该实例。
  • 当进行赋值如 setValue:forKey: 时,如果 key 的数据类型非对象,则会发送一条 <type>Value 消息给 value 对象以提取基础数据,然后赋值给 key。

注意:因为Swift 中的所有属性都是对象,所以这里仅适用于 Objective-C 属性。
当进行赋值如 setValue:forKey: 时,如果 key 的数据类型是非对象类型,则 value 就禁止传 nil。否则会调用 setNilValueForKey: 方法,该方法的默认实现抛出异常 NSInvalidArgumentException,并导致程序 Crash。

5.属性验证

KVC 提供了属性验证的方法,如下。我们可以在使用 KVC 赋值前验证能否为这个 key 赋值指定 value。
validateValue 方法的默认实现是查看消息接收者类中是否实现了遵循命名规则为 validate<Key>:error: 的方法,如果有的话就返回调用该方法的结果;如果没有的话,则默认验证成功并返回 YES。我们可以在消息接收者类中实现 validate<Key>:error: 的方法来自定义逻辑返回 YES 或 NO。

#import <Foundation/Foundation.h>
#import "DJTestModelOne.h"
@interface DJTestModel : NSObject
@property (nonatomic,strong) NSArray <DJTestModelOne *> *oneArray;
@property (nonatomic,copy)NSString *name;
@end


#import "DJTestModel.h"

@implementation DJTestModel

-(BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError{
    NSString *name = *ioValue;
        BOOL result = NO;
        if ([name isEqualToString:@"dj"]) {
            result = YES;
        }
        return result;
}

@end
DJTestModel *model = [[DJTestModel alloc]init];

    NSString *value = @"dj";
    NSString *key = @"name";
    NSError  *error;
    BOOL result = [model validateValue:&value forKey:key error:&error];
       
   if (error) {
       NSLog(@"error = %@", error);
   }

三、原理

1.搜索规则

除了了解 KVC 的使用,了解 KVC 取值和赋值过程的工作原理也是很有必要的。

(1)基本的 Getter 搜索模式

以下是 valueForKey: 方法的默认实现,给定一个 key 作为输入参数,在消息接收者类中操作,执行以下过程。

  • 第一步:按照 get<Key><key>is<Key>_<key> 顺序查找方法。如果找到就调用取值并执行第五步,否则执行第二步;
  • 第二步:查找 countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes: 命名的方法。如果找到第一个和后面两个中的至少一个,则创建一个能够响应所有 NSArray 的方法的集合代理对象(类型为 NSKeyValueArray,继承自 NSArray),并返回该对象。否则执行第三步;
    代理对象随后将其接收到的任何 NSArray 消息转换为 countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes: 消息的组合,并将其发送给 KVC 调用方。如果原始对象还实现了一个名为 get<Key>:range: 的可选方法,则代理对象也会在适当时使用该方法。
    当 KVC 调用方与代理对象一起工作时,允许底层属性的行为如同 NSArray 一样,即使它不是 NSArray
  • 第三步:查找 countOf<Key>enumeratorOf<Key>memberOf<Key>: 命名的方法。如果三个方法都找到,则创建一个能够响应所有 NSSet 的方法的集合代理对象(类型为 NSKeyValueSet,继承自 NSSet),并返回该对象。否则执行第四步;
    代理对象随后将其接收到的任何 NSSet 消息转换为 countOf<Key>enumeratorOf<Key>memberOf<Key>: 消息的组合,并将其发送给 KVC 调用方。
    当 KVC 调用方与代理对象一起工作时,允许底层属性的行为如同 NSSet 一样,即使它不是 NSSet
  • 第四步:查看消息接收者类的 +accessInstanceVariablesDirectly 方法的返回值(默认返回 YES)。如果返回 YES,就按照 _<key>_is<Key><key>is<Key> 顺序查找成员变量。如果找到就直接取值并执行第五步,否则执行第六步。如果 +accessInstanceVariablesDirectly 方法返回 NO 也执行第六步。
  • 第五步:如果取到的值是一个对象指针,即获取的是对象,则直接将对象返回。如果取到的值是一个NSNumber 支持的数据类型,则将其存储在 NSNumber 实例并返回。如果取到的值不是一个 NSNumber 支持的数据类型,则转换为 NSValue 对象, 然后返回。
  • 第六步:调用 valueForUndefinedKey: 方法,该方法抛出异常 NSUnknownKeyException,并导致程序 Crash。这是默认实现,我们可以重写该方法根据特定 key 做一些特殊处理。
(2)基本的 Setter 搜索模式

以下是 setValue:forKey: 方法的默认实现,给定 key 和 value 作为输入参数,尝试将 KVC 调用方的属性名为 ke y的值设置为 value,执行以下过程。

  • 第一步:按照 set<Key>:_set<Key>: 顺序查找方法。如果找到就调用并将 value 传进去(根据需要进行数据类型转换),否则执行第二步。
  • 第二步:查看消息接收者类的 +accessInstanceVariablesDirectly 方法的返回值(默认返回 YES)。如果返回 YES,就按照 _<key>_is<Key><key>is<Key> 顺序查找成员变量(同基本的 Getter 搜索模式)。如果找到就将 value 赋值给它(根据需要进行数据类型转换),否则执行第三步。如果 +accessInstanceVariablesDirectly 方法返回 NO 也执行第三步。
  • 第三步:调用 setValue:forUndefinedKey: 方法,该方法抛出异常 NSUnknownKeyException,并导致程序 Crash。这是默认实现,我们可以重写该方法根据特定 key 做一些特殊处理。
2.异常处理

根据 KVC 搜索规则,当没有搜索到对应的 key 或者 keyPath 相关方法或者变量时,会调用对应的异常方法 valueForUndefinedKey:setValue:forUndefinedKey:,这两个方法的默认实现是抛出异常 NSUnknownKeyException,并导致程序 Crash。我们可以重写这两个方法来处理异常。

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

当进行赋值如 setValue:forKey: 时,如果 key 的数据类型是非对象类型,则 value 就禁止传 nil。否则会调用 setNilValueForKey: 方法,该方法的默认实现是抛出异常 NSInvalidArgumentException,并导致程序 Crash。我们可以重写这个方法来处理异常。

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

推荐阅读更多精彩内容

  • KVC 简介 KVC全称是Key Value Coding(键值编码),是一个基于NSKeyValueCoding...
    rightmost阅读 825评论 0 0
  • KVC KVC定义 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过K...
    jackyshan阅读 51,930评论 9 200
  • 前言: 本文基本不讲KVC/KVO的用法,只结合网上的资料说说对这种技术的理解。 由于KVO内容较少,而且是以KV...
    土b兰博王阅读 3,078评论 0 33
  • 一、基本介绍 KVC官网地址 KVC全称为key value coding,简称键值编码,它是一种机制,使用NSK...
    默默_David阅读 290评论 0 2
  • KVC定义 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名直...
    SheIsMySin_72e7阅读 388评论 0 0