简单易懂KVC基础篇

博客日志:2019-3-14 起笔。
博客日志:2019-3-22 根据我的阅读感觉对文章大幅重构。
博客日志:2019-3-25 封笔。html

引言

这篇文章其实就是被他的兄弟KVO给逼出来的,没办法。官方文档中介绍过KVC是KVO技术实现的基础,闲话免提,我们请入座。学识有限,有不对的地方,还请你们多多指正。编程

概述

KVC(Key-value coding)键值编码是一种由NSKeyValueCoding非正式协议(其实就是咱们所说的分类或类别)启用的机制,对象采用该机制提供对其属性间接访问。当对象符合键值编码时,其属性可以使用字符串参数经过简洁,统一的消息传递接口(方法)寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。数组

键值编码是一个基本概念,是许多其余Cocoa技术的基础,例如KVO,(macOS)Cocoa绑定,Core Data和AppleScript。在某些状况下,键值编码还有助于简化代码。app

这里咱们搞了段很官方的描述,其实简单来讲的话,就是经过字符串名称访问对象属性,就这么简单。性能

API接口

普通用法

访问对象属性

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
复制代码

KVC提供了简洁,统一的方法,用来访问对象属性。分别是对应于getter访问器的valueForKey:和对应于setter访问器的setValue:forKey:。幸运的是,NSObject采用了NSKeyValueCoding协议并为它们和其余基本方法提供默认实现。所以,若是你从NSObject(或其许多子类中的任何一个)派生对象,那么大部分都工做已经完成了。学习

@interface BankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
@end

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@end
复制代码

如今咱们声明了两个类来讲明KVC的基础用法,咱们假设BankAccount的实例对象是myAccount,一般咱们会直接使用访问器方法操做属性。测试

myAccount.currentBalance = @(100.0);
// 或者
[myAccount setCurrentBalance:@(100.0)];
复制代码

固然咱们知道上面两个方法是等价的。如今咱们看一下KVC的使用方式:优化

// setter
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
// getter
NSNumber *currentBalance = [myAccount valueForKey:@"currentBalance"];
复制代码

按键路径访问属性

若是咱们想要获取银行帐户户主的姓名,咱们能够在引入Person.h以后,使用点语法很轻松的获取到:ui

NSString *myName = myAccount.owner.name;
复制代码

固然KVC也提供了咱们访问属性的属性的操做方法,经过键路径来访问属性。键路径是以点分隔多个键的字符串用来指定要遍历的对象属性的序列。序列中第一个键是相对于接收者的属性,而且每一个后续键是相对于前一个键的属性。编码

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
复制代码

如今咱们可使用键路径访问属性了:

NSString *myName = [myAccount valueForKeyPath:@"owner.name"];
[myAccount setValue:@"SepCode" forKeyPath:@"owner.name"];
复制代码

键未定义异常

根据KVC规定的方式(搜索模式)找不到由key命名的属性时,就会调用获取值的valueForUndefinedKey:或设置值的setValue:forUndefinedKey:方法,系统默认的该方法会引起一个 NSUndefinedKeyException的异常致使崩溃,咱们能够重写该方法避免崩溃。而且咱们也能够在重写该方法时,加入逻辑处理以使其更加的优雅。

// 重写UndefinedKey:方法
// getter
- (id)valueForUndefinedKey:(NSString *)key {
    return nil;
}
// setter
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    
}
复制代码

非对象值和nil

当咱们经过setValue:forKey:对属性赋值,若是该属性不是对象而是标量或结构体时,KVC会自动展开对象获取值并赋值给属性。一样当执行valueForKey:时,则会自动包装属性值,返回一个与其对应的NSNumber或NSValue对象。

// setter
[owner setValue:@(26) forKey:@"age"];
// getter
NSNumber *myAge = [owner valueForKey:@"age"];
复制代码

当咱们给对象赋值nil时,这很容易理解,表示把对象设置为空。可是当咱们经过setValue:forKey:设置非对象属性值为nil时,没有对象可展开了,难道咱们都把这些非对象值设置为0吗?官方并无给咱们实现默认的赋值操做,而是调用setNilValueForKey:方法,而系统默认的该方法会引起一个NSInvalidArgumentException的异常,固然咱们也能够重写该方法实现特定的行为。

// nil
[owner setValue:nil forKey:@"age"];
...
- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        [self setValue:@(0) forKey:@”age”];
    } else {
        [super setNilValueForKey:key];
    }
}
复制代码

多值访问

咱们看到官方还提供了dictionary相关的方法,但他并非针对字典的方法。而是同时访问多个属性的方法,其实就是调用每一个key的setValue:forKey:valueForKey:方法,这很容易理解咱们再也不赘述。

NSDictionary *dict = [owner dictionaryWithValuesForKeys:@[@"name",@"age"]];
dict = @{@"name":@"sepCode",@"age":@(62)};
[owner setValuesForKeysWithDictionary:dict];
复制代码

特殊用法

访问集合属性

咱们前面讲述了KVC访问对象的方式,固然它也一样适用于集合对象。你能够像使用任何其余对象同样,经过valueForKey:setValue:forKey:(或它们的键路径方式)获取或设置集合对象。

@interface Transaction : NSObject
 
@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When
 
@end
复制代码

如今咱们又定义了一个交易类,假如咱们想获取我的银行帐户中的全部收款人。

NSArray *payees = [myAccount valueForKeyPath:@"transactions.payee"];
复制代码

请求transactions.payee键路径的值将返回一个数组,包含transactions中全部的payee对象。这也适用于键路径中的多个数组。假如咱们想获取多个银行帐户中的全部收款人,请求键路径accounts.transactions.payee的值返回一个数组,其中包含全部账户中全部交易的全部收款人对象。

对于获取值咱们看到了KVC的方便之处,可是对于设置值咱们却不多用到KVC。它会把集合内包含的全部键对象的值设置为相同的值,这不是咱们想要的结果。

虽然咱们可使用通用的方式访问集合对象,可是,当你想要操纵这些集合的内容时,官方推荐咱们最有效的方法是使用协议定义的可变代理方法。 协议为访问集合对象定义了三种不一样的代理方法,每种方法都有key和keyPath变种: mutableArrayValueForKey:mutableArrayValueForKeyPath: 它们返回一个行为相似于NSMutableArray对象的代理对象。 mutableSetValueForKey:mutableSetValueForKeyPath: 它们返回一个行为相似于NSMutableSet对象的代理对象。 mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath: 它们返回一个行为相似于NSMutableOrderedSet对象的代理对象。

当你对代理对象进行操做,添加对象,从中删除对象或替换对象时,协议的默认实现会相应地修改原对象。如今假如咱们想使用KVC通用方法,在我的银行帐户增长一次交易,经过valueForKey:获取非可变集合对象,建立可变集合对象增长内容,而后使用setValue:forKey:消息将其存储回对象。相比之下经过代理对象操做,就显得方便不少。在许多状况下,它比直接使用可变属性更有效。例如,当咱们不使用常量字符串做为key,而是使用变量时。这容许咱们没必要知道调用方法的确切名称,只要对象和正在使用的key符合KVC,一切都会正常工做。 当维护集合中对象时,这些方法还使其能够支持键值观察机制。这也是为何KVO的文章写到一半时,我又忽然先来写KVC了。

这里咱们须要注意的是,这些方法的做用是返回一个集合对象的代理对象。固然你也能够像咱们以前讲到的同样,请求集合内对象的属性,从而达到返回一个属性集合对象,但这仅仅局限于获取值。若是这种状况下操做属性集合对象原集合内的对象的属性的值就会被设置为操做后的属性集合对象,这也不是咱们想要的结果。

使用集合运算符

当你向符合键值编码的对象发送valueForKeyPath:消息时,或者表述为当对象调用valueForKeyPath:方法时,能够在键路径中嵌入集合运算符。集合运算符是一个前面是at符号(@)的关键字,它指定了getter应该执行的操做,以便在返回以前以某种方式操做数据。NSObject为此行为提供了默认实现。

当键路径包含集合运算符时,运算符以前的键路径(称为左键路径)指示相对于消息接收者操做的集合。若是将消息直接发送到集合对象(例如NSArray实例),则能够省略左键路径。操做符以后的键路径部分(称为右键路径)指定操做员应处理的集合中的属性。除了@count以外,全部集合运算符都须要右键路径。

集合运算符键路径格式

集合运算符的表现行为可分为三种基本类型:

  • 聚合运算符以某种方式合并集合的对象,并返回一般与右键路径中指定的属性的数据类型匹配的单个对象。@count是一个例外,它没有右键路径即使是有也会被忽略并始终将返回一个NSNumber实例。

    NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
    NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
    NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
    复制代码
  • 数组运算符返回与右键路径指示的特定对象集相对应的对象数组。

  • 嵌套操做符处理包含其余集合的集合,并根据操做符返回一个NSArray或NSSet实例,它以某种方式组合嵌套集合的对象。

具体运算符用法,请点击上述各种型超连接在官方文档中查看。

属性验证

键值编码协议定义了支持属性验证的方法。就像使用KVC通用方法同样,你也能够按键(或键路径)验证属性。当你调用validateValue:forKey:error:(或validateValue:forKeyPath:error:)方法时,协议的默认实现会使对象实例搜索是否实现了validate<Key>:error:方法。若是对象没有实现此类方法,则默认验证成功,并返回YES。

一般可采用如下验证方式:

  • 当值对象有效时,返回YES,不更改值对象或错误。

  • 当值对象无效时,而且你不能或不想提供有效的替代方法,设置错误缘由NSError而且返回NO。

  • 当值对象无效但你知道有效的替代方法时,建立有效对象,将值引用分配给新对象,而后返回YES,不设置NSError错误。若是提供其余值,则始终返回新对象,而不是修改正在验证的对象,即便原始对象是可变的。

Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
    NSLog(@"%@",error);
}
...

- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
    if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
        if (outError != NULL) {
            *outError = [NSError errorWithDomain:PersonErrorDomain
                                            code:PersonInvalidNameCode
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"Name too short" }];
        }
        return NO;
    }
    return YES;
}
复制代码

上述用例演示了一个name字符串属性的验证方法,该方法确保值对象的最小长度和不为nil。若是验证失败,此方法不会替换其余值。

原理解析

访问者搜索模式

KVC协议中最关键的部分就是访问者搜索模式,NSObject提供的NSKeyValueCoding协议的默认实现,使用明肯定义的规则集将基于键的访问器(KVC存取方法)调用映射到对象的属性。这些协议方法使用键参数在其本身的对象实例中搜索访问器,实例变量以及遵循某些命名约定的相关方法。

可变数组的搜索模式

这里咱们仅介绍一种模式可变数组的搜索模式,其余搜索模式可经过访问者搜索模式了解详细内容。

mutableArrayValueForKey:的默认实现,输入一个键参数,返回一个可变代理数组。对象内部的名为key的属性,经过如下过程接受访问器的调用:

  1. 查找一对方法名如insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:(分别对应于NSMutableArray的基本方法insertObject:atIndex:removeObjectAtIndex:)或名称相似于insert<Key>:atIndexes:remove<Key>AtIndexes:的方法(对应于NSMutableArrayinsertObjects:atIndexes:removeObjectsAtIndexes:方法)。

    若是对象具备至少一个插入方法和至少一个删除方法,返回一个代理对象来响应这些NSMutableArray的消息。经过发送一些组合的消息insertObject:in<Key>AtIndex:, removeObjectFrom<Key>AtIndex:, insert<Key>:atIndexes:,和remove<Key>AtIndexes:mutableArrayValueForKey:消息的接受者来实现。 或者能够表述为经过使调用mutableArrayValueForKey:方法的对象,调用上述方法,来响应这些插入或删除方法。

    当接收mutableArrayValueForKey:消息的对象也实现名称为replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:的(可选)替换方法时,代理对象也会在适当时使用这些方法以得到最佳性能。

  2. 若是对象没有可变数组的方法,查找名称与模式集匹配的set<Key>:的访问器方法。在这种状况下,返回一个代理对象。经过向mutableArrayValueForKey:的原始接收者发出set<Key>:消息,来响应上述那些NSMutableArray的消息。

    注意:前两步简单来讲就是代理对象操做集合内容时,先去查找是否实现了插入,删除,(可选)替换的方法,没实现就去查找setter方法。步骤2中描述的机制比前一步骤的效率低得多,由于它可能涉及重复建立新的集合对象而不是修改现有的集合对象。所以,在设计本身的符合键值编码的对象时,一般应该避免使用它。

  3. 若是既未找到可变数组方法,也未找到访问器,而且接收者的类对accessInstanceVariablesDirectly的响应为YES,表示容许搜索实例变量,则按顺序搜索名称为_<key><key>的实例变量。 若是找到这样的实例变量,则返回一个代理对象,该对象将它接收的每一个NSMutableArray消息转发给实例变量,一般是NSMutableArray或其子类之一的实例。

  4. 若是全部其余方法都失败了,则返回一个可变集合代理对象,该对象在收到NSMutableArray消息时向mutableArrayValueForKey:消息的原始接收者发出setValue:forUndefinedKey:消息。 setValue:forUndefinedKey:的默认实现会引起NSUndefinedKeyException异常。

    注意:后两步简单来讲就是,若是容许搜索实例变量,就去查找变量,若是以上搜索都失败,就报错。

原理实践

如今咱们根据可变数组的搜索模式,作一些实践和测试:

@interface ViewController ()
/// array
@property (nonatomic, strong) NSMutableArray *array;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    self.array = [@[@(1),@(2),@(3)] mutableCopy];
    NSMutableArray *kvcArray = [self mutableArrayValueForKey:@"array"];
    // 发送NSMutableArray消息
    [kvcArray addObject:@(4)];
    [kvcArray removeLastObject];
    [kvcArray replaceObjectAtIndex:0 withObject:@(4)];
    
}
// 可变数组多对多优化
- (void)insertObject:(NSNumber *)object inArrayAtIndex:(NSUInteger)index {
    [self.array insertObject:object atIndex:index];
}

- (void)removeObjectFromArrayAtIndex:(NSUInteger)index {
    [self.array removeObjectAtIndex:index];
}

- (void)replaceObjectInArrayAtIndex:(NSUInteger)index withObject:(id)object {
    [self.array replaceObjectAtIndex:index withObject:object];
}

@end
复制代码

上图的测试结果,向咱们展示了若是咱们使用代理对象时,最好实现完整协议,优化多对多关系,不然随着数据量级增长,性能会呈指数级降低,这真的很糟糕。

疑点解惑

在这里我要说一下我对于kvc是kvo实现的基础的理解。由于在网上看到一位文章写的还不错的做者,他讲到两者实现机制不一样,并没有必然联系,只是KVC对KVO的支持比较好。我很是不一样意这个观点。在官方键值观察编程指南中明确指出,该类的属性必须遵照KVC合规性。KVC是一个经过字符串访问对象属性的协议,包括搜索模式也属于该协议的一部分。KVO观察的属性,必须遵照KVC合规性,而且支持观察KVC兼容的全部访问器修改属性。一般咱们所理解的KVO都是基于setter访问器实现的,然而并不是如此。下图也充分验证KVO支持KVC的搜索模式:

这里让我想到了饿了么技术沙龙中兰建刚的忠告:中文博客-在你没有能力分辨对错以前,少看。

结语

这篇文章呢,写着写着我就又有感慨了。我深深的感觉到,我是一个学习者,这些知识都是别人创造的,用的都是别人提供给咱们的方法。就连学习也多是靠他人总结的,我还不是一个创造者。

不过认清本身是多么菜,也没什么很差的。即使一样处于学习阶段的他人,也能够成为本身的老师,但愿你们能够多多指点迷津。

最近看了很多他人的文章,我从本身的感觉发现几点。

  • 喜欢做者把技术经过图或者文字表述的很清楚,不喜欢看做者大段的代码来表述,可是简单的用例仍是必须的。
  • 不要一会儿把接口全列出来,最多扫一眼,除非做者的目的也是你就瞄一眼就能够了。因此讲解时的顺序能够是表述,接口,用例。一个点一个点的展开。
  • 文章结构清晰,不要天上一脚,地上一脚,因此前提是做者思路清晰。

另外有大牛建议不须要看太多书,经典的书多读几遍,独立思考。本篇文章基本是在多看官方文档的基础上诞生的,本人对于细节知识仍是比较在乎的,若是有理解不对的地方,还请你们多多指正。

相关文章
相关标签/搜索