原理探究

KVC原理探究

2019-04-29  本文已影响0人  barry

KVC

iOS在实际开发过程中用KVC的地方也是不少的,但是很少有时间探究里面涵盖的内容,网上的一些文章也纯属是翻译的官方文档。有些细节根本没有验证完全, 本文着重探索一下里面的实现细节。

KVC.png

一、KVC 定义

       KVC全称Key Value Coding,具体来说就是KVC是由NSKeyValueCoding非正式协议启用的机制,对象采用该机制提供对其属性的间接访问。当对象符合键值编码时(通常在OC中,继承NSObject即可),其属性可通过字符串参数通过简洁,统一的消息传递接口来存取消息。它属于间接访问对象的属性,区别于属性的直接访问方法settergetter,亦或直接访问成员变量.因为是间接访问属性,所以KVC性能比不上直接存取属性的方法,但是可以提高程序的灵活性。必要的时候可以减少冗余代码。相关参考KVC官方文档

二、NSKeyValueCoding

图片.png

三、基本操作

KVC对属性的操作主要分为:
基础属性--标量(整型,浮点型等),字符串,布尔类型,NSNumber, NSColor等
一对一关系的属性--当前对象有一个对象属性,对象属性在改变自己内部属性的时候,对象属性不会改变
一对多关系的属性--通过NSArray或NSSet包含的其它对象集合

3.1 基础属性

//银行账户
@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

使用属性字符串作key,来查找当前的属性

//存取普通属性 An attribute
NSLog(@"currentBalance--%@", [self.account valueForKey:@"currentBalance"]);//通过key获取属性
[self.account setValue:@(111) forKey:@"currentBalance"];                   //通过key设置属性值
NSLog(@"currentBalance--%@", [self.account valueForKey:@"currentBalance"]);//通过key获取属性

3.2 一对一关系的属性和Key Paths

// 个人信息
NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;

@end

可以通过key path 获取BankAccount对象中的属性person,进而获取Personname属性

//一对一属性 to - one
NSLog(@"name--%@", [self.account valueForKeyPath:@"owner.name"]); //通过keypath获取属性
[self.account setValue:@"NewName" forKeyPath:@"owner.name"];      //通过keypath设置属性值
NSLog(@"name--%@", [self.account valueForKeyPath:@"owner.name"]); //通过keypath获取属性

       所谓Key Paths 是指由一串点分隔键组成的字符串,用于指定要遍历的对象属性序列。字符串序列中的第一个键的属性是相对于接收者的(owner是 self.account的属性,接收者是self.account),子序列中的第一个键的属性是相对于前一个属性的(name是owner的属性)

3.3 一对多关系的属性

       当给键值编码兼容对象发送valueForKeyPath:消息时,可以在键路径中嵌入一个集合运算符。 集合运算符是一个小符号列表之一,前面是一个符号(@),它指定了getter在返回之前应该以某种方式操作数据的操作。 NSObject提供的valueForKeyPath:的默认实现实现了这种行为。其格式如下:

keypath.jpg

3.3.1 Aggregation Operators聚合操作符

       聚合运算符以某种方式合并集合的对象,并返回通常与右键路径中指定的属性的数据类型匹配的单个对象。 @count运算符是一个例外 - 它没有正确的键路径,总是返回一个NSNumber实例。

NSNumber *transactionAverage = [self.account.transactions valueForKeyPath:@"@avg.amount"];
NSNumber *numberOfTransactions = [self.account.transactions valueForKeyPath:@"@count"];
NSDate *latestDate = [self.account.transactions valueForKeyPath:@"@max.date"];
NSDate *earliestDate = [self.account.transactions valueForKeyPath:@"@min.date"];
NSNumber *amountSum = [self.account.transactions valueForKeyPath:@"@sum.amount"];

3.3.2 Array Operators数组操作符

数组操作符通过调用valueForKeyPath和right key path 指示的对象返回一个特定的序列

NSArray *distinctPayees = [self.account.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
NSArray *distinctPayees = [self.account.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];

3.3.3 Nesting Operators嵌套操作符

嵌套操作作用在一个嵌套的序列上,序列中的每一个元素也是一个序列。

NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
NSSet *set1 = [NSSet setWithArray:self.account.transactions];
NSSet *set2 = [NSSet setWithArray:moreTransactions];
NSSet *set = [NSSet setWithObjects:set1, set2, nil];
NSSet *collectedSetPayees = [set valueForKeyPath:@"@distinctUnionOfSets.payee"];
下面是基本的存取方法的简介:

注: 非对象属性通过KVC设置值,如果值nil,会调用setNilValueForKey:,其内部触发一个异常,可以根据需要重写它来进行其它操作

四、非对象类型的表示

KVC支持scalar和结构体,对于KVC中的value,必须是一个对象类型,所以scalar和结构体必须包裹成相应的对象。

[self.account setValue:[NSNumber numberWithInt:10] forKey:@"no"];
[self.account setValue:@(10) forKey:@"no"];
typedef struct {
float x, y, z;
} ThreeFloats;

NS_ASSUME_NONNULL_BEGIN

@interface StructValueTest : NSObject

@property (nonatomic) ThreeFloats threeFloats;

@end

StructValueTest *myClass = [[StructValueTest alloc] init];
NSValue *result = [myClass valueForKey:@"threeFloats"]; 
ThreeFloats temp;
[result getValue:&temp];   //通过getValue获取真实的类型
NSLog(@"修改前x-%f,y-%f,z-%f", temp.x, temp.y, temp.z);

ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"]; //设置新值
result = [myClass valueForKey:@"threeFloats"]; 
[result getValue:&temp];   //通过getValue获取真实的类型
NSLog(@"修改前x-%f,y-%f,z-%f", temp.x, temp.y, temp.z);

五、属性的验证

Key-value coding protocol支持属性验证,但是不会自动地调用验证方法(如果自动调用肯定会消耗性能),必须根据自己的需求在合适的时机调用 validateValue:forKey:error:。但是默认的方法始终返回Yes,所以无法验证,必须写一个validate<Key>:error:方法也实现具体的验证思路。

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 **)outError {
//这里只判断类型是不是NSString类型,是返回true,否返回false
NSString *result = (NSString *)*ioValue;
if ([result isKindOfClass:[NSString class]]) {
return true;
}
NSError *error = [[NSError alloc] initWithDomain:@"0" code:100 userInfo:@{@"info":@"type error"}];
*outError = error;
return false;
}
//属性验证
- (void)validateProperty {
Person* person = [[Person alloc] init];
NSError* error;
//    NSString* name = @"John";
NSNumber *num = [NSNumber numberWithInt:12];
if (![person validateValue:&num forKey:@"name" error:&error]) {
NSLog(@"%@",error);
}
}

验证结果:
2019-04-29 01:41:08.969260+0800 KVC[6294:243510] Error Domain=0 Code=100 "(null)" UserInfo={info=type error}

六、基本Getter方法的搜索模式

NSObject提供的NSKeyValueCoding协议的默认实现使用明确定义的一套规则将基于Key的访问器的调用映射到对象的内部属性。也就是说内部有自己的一套访问存取方法,实例变量和其它的一些相关方法。
这是valueForKey:的默认实现,给定一个key当做输入参数,开始下面的步骤,在这个接收valueForKey:方法调用的类内部进行操作。

搜索路径如下图:

搜索路径.png

七、基本Setter搜索模式

这是setValue:forKey:的默认实现,根据给定的key设置相应的value

八、可变数组搜索模式

mutableArrayValueForKey:根据给定的key返回一个可变的value(可变数组)
mutableArrayValueForKey:内部调用顺序如下:

搜索路径如下图:

图片.png

九、可变有序Set搜索模式

mutableOrderedSetValueForKey:根据给定的key返回一个可变的value(有序可变Set)
其实现流程和可变数组大致一致,不再赘述,可看demo

十、可变Set搜索模式

mutableSetValueForKey:,根据给定的key返回一个可变的value(可变Set)
其实现流程和可变数组大致一致,这里只给一个流程图,具体可查看demo

图片.png

十一、KVC其它操作

class User: NSObject{
var name:String = ""  
var age:Int = 0 
}
let user = User()
user.name = "hangge"
user.age = 100
//取值
let name = user1[keyPath: \User.name]
print(name)
//设置值
user1[keyPath: \User.name] = "hangge.com"
上一篇 下一篇

猜你喜欢

热点阅读