KVC

2020-02-16  本文已影响0人  瞬间完善

我们可以通过苹果官方文档看到KVC的解释:

键值编码是NSKeyValueCoding非正式协议支持的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象符合键值编码时,可通过简洁,统一的消息传递接口通过字符串参数访问其属性,这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。

通常,您使用访问器方法来访问对象的属性。一个get访问器(或getter)从一个属性返回值,一个set访问器(或setter)给一个属性设置值。在Objective-C中,您还可以直接访问属性的基础实例变量。以任何一种方式访问​​对象属性都很简单,但是需要调用特定于属性的方法或变量名。随着属性列表的增加或更改,访问这些属性的代码也必须如此。相反,与键值编码兼容的对象提供了一个简单的消息传递接口,该接口在其所有属性之间都是一致的。

键值编码是许多其他Cocoa技术的基础概念,例如键值观察,Cocoa绑定,Core DataAppleScript-ability。在某些情况下,键值编码还可以帮助简化代码。

在实现了访问器方法的类中,使用点语法和KVC访问对象其实差别不大,二者可以任意混用。但是没有访问器方法的类中,点语法无法使用,这时KVC就有优势了。

1、访问对象的属性

属性:这些是简单的值,例如标量,字符串或布尔值。值对象(例如NSNumber)和其他不可变类型(例如NSColor)也被视为属性。
一对一的关系:这些是具有自己属性的可变对象。对象的属性可以更改,而无需更改对象本身。例如,银行帐户对象可能具有所有者属性,该属性是Person对象的实例,而Person对象本身具有address属性。所有者的地址可以更改,而无需更改银行帐户持有的所有者属性。
一对多关系:这些是集合对象。尽管也可以使用自定义集合类,但是通常使用NSArrayNSSet的实例来保存此类集合。

@interface YXPerson : NSObject
 
@property (nonatomic) NSNumber* accountNumber;              // 一个属性
@property (nonatomic, strong) YXStudent *student;           // 一对一的关系
@property (nonatomic) NSArray< Transaction* >* transactions; // 一对多的关系
 
@end

为了维护封装,对象通常为其接口上的属性提供访问器方法。 对象的作者可以显式地编写这些方法,也可以依靠编译器自动合成它们。 无论哪种方式,使用这些访问器之一的代码作者都必须在编译属性名称之前将其写入代码。 访问器方法的名称成为使用它的代码的静态部分。 例如:

// ✅ 在YXPerson.h中定义一个accountNumber属性
@interface YXPerson : NSObject
@property (nonatomic, assign) NSNumber * accountNumber;
@end
************************
// ✅ 在YXPerson.m中
- (NSNumber*) setAccountNumber{
    return self. accountNumber;
}
************************
// ✅ 在使用的地方:
[person setAccountNumber:@19];
NSLog(@"accountNumber - %@",person. accountNumber);
************************
// ✅ 输出
2020-02-15 18:53:32.792859+0800 KVC简用[16886:179545] accountNumber - 19

这是最直接的,但缺乏灵活性。 另一方面,符合键值编码的对象提供了一种更通用的机制,可以使用字符串标识符访问对象的属性。

1.1、使用KeyKeyPaths径识别对象的属性

key是标识特定属性的字符串。 通常,按照约定,代表属性的键是该属性本身在代码中出现的名称。key必须使用ASCII编码,不能包含空格,并且通常以小写字母开头(尽管有例外,例如在许多类中找到的URL属性)。

由于上面代码中的YXPerson类符合键值编码,因此它可以识别这些key键的accountNumberstudenttransactions,这是其属性的名称。 您可以通过其键设置值,而不是调用setAccountNumber:方法:

[person setValue:@19 forKey:@"accountNumber"];
1.2、使用key获取属性的值

当对象采用NSKeyValueCoding协议时,它符合键值编码。继承自NSObject的对象(提供了该协议的基本方法的默认实现)会自动采用具有某些默认行为的该协议。这样的对象至少实现以下基于键的基本getter

集合对象(例如NSArrayNSSetNSDictionary)不能包含nil作为值。 而是使用NSNull对象表示nil值。NSNull提供了单个实例,表示对象属性的nil值。 dictionaryWithValuesForKeys:和相关的setValuesForKeysWithDictionary:的默认实现会自动在NSNull(在dictionary参数中)和nil(在存储的属性中)之间转换。

1.3、使用key设置属性的值

getter一样,与键值编码兼容的对象还根据NSObject中提供的NSKeyValueCoding协议的实现,为一小组具有默认行为的定义setter

在默认实现中,当您尝试将非对象属性设置为nil值时,符合键值编码的对象会向自身发送setNilValueForKey:消息。 setNilValueForKey:的默认实现抛出NSInvalidArgumentException,但是对象重写这个方法,以设置默认值或标记值。
例如:

YXPerson *person = [[YXPerson alloc] init];
    
    // 1:Key-Value Coding (KVC) : 基本类型
    [person setValue:@19 forKey:@"accountNumber"];
    
    NSLog(@"person - %@",person.accountNumber);
    
// ✅ KeyPath赋值
    YXStudent *student = [[YXStudent alloc] init];
    person.student   = student;
    [person setValue:@"KP" forKeyPath:@"student.name"];
    NSLog(@"student - %@ ",[person valueForKeyPath:@"student.name"]);
**********************
// ✅ 输出
2020-02-15 19:46:54.164662+0800 KVC简用[20478:221482] person - 19
2020-02-15 19:46:54.164921+0800 KVC简用[20478:221482] student - KP

2、访问集合属性

就像访问和设置其他属性一样,您也可以使用valueForKey:setValue:forKey:来访问和设置集合属性的值。但是,当您要操纵这些集合的内容时,通常使用协议定义的可变代理方法最有效。

该协议定义了三种不同的代理对象访问代理方法,每种方法都有一个键和一个键路径变量:

// 2: KVC - 集合类型
    person.array = @[@"1",@"2",@"3"];
    // ✅ 不可变数组
    NSArray *ary = [person valueForKey:@"array"];
    ary = @[@"100",@"2",@"3"];
    [person setValue:ary forKey:@"array"];
    NSLog(@"%@",[person valueForKey:@"array"]);
    // ✅ 可变数组
    NSMutableArray *muAry = [person mutableArrayValueForKey:@"array"];
    muAry[0] = @"200";
    NSLog(@"%@",[person valueForKey:@"array"]);
*************************
// ✅ 输出
2020-02-16 11:14:12.620737+0800 KVC简用[3041:31480] (
    100,
    2,
    3
)
2020-02-16 11:14:12.623391+0800 KVC简用[3041:31480] (
    200,
    2,
    3
)

3、集合运算符

当您发送与键值编码兼容的对象valueForKeyPath:消息时,可以将集合运算符嵌入到键路径中。集合运算符是一小部分关键字之一,其后带有一个at符号(@),该符号指定getter在返回数据之前应执行的操作以某种方式处理数据。由NSObject提供的valueForKeyPath:的默认实现会实现此行为。

4、类型转换

当您调用协议的一种getters,例如valueForKey:时,默认实现将根据访问者搜索模式中描述的规则来确定为指定键提供值的特定访问器方法或实例变量。 如果返回值不是对象,则getter使用此值初始化NSNumber对象(用于标量)或NSValue对象(用于结构体),并返回该值。

类似地,默认情况下,使用setValue:forKey之类的setter:在给定特定键的情况下,确定属性的访问器或实例变量所需的数据类型。 如果数据类型不是对象,则设置器首先将适当的<type> Value消息发送到传入值对象以提取基础数据,然后存储该数据。

WX20200213-180329@2x.png
WX20200213-180313@2x.png

4.1、自定义结构体类型的转换

typedef struct {
    float x, y, z;
} ThreeFloats;
 
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end

通过KVC取值的时候,通过NSValue获取

NSValue* result = [myClass valueForKey:@"threeFloats"];

通过NSValue来进行包装一下,然后再通过KVC赋值。

ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];

5、KVC的底层原理

5.1、getter方法查找

当一个对象调用valueForKey:方法取值的时候,他的内部执行以下过程。

// ✅ YXPerson.h
@property (nonatomic, copy) NSString *name;
**********************
// ✅ YXPerson.m,可以每一次注释一个,看输出
- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}
**********************
// ✅ 取值
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
    
NSLog(@"取值:%@",[person valueForKey:@"name"]);
**********************
// ✅ 输出
2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:getName
2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:name
2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:isName
2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:_name

第二点:

// ✅ YXPerson.h
@property (nonatomic, strong) NSArray *array;
**********************
// ✅ YXPerson.m
- (NSInteger)countOfNames{
    return self.array.count;
}
- (id)objectInNamesAtIndex:(NSUInteger)index{
    return _array[index];
}
**********************
// ✅ 取值
person.array = @[@"1",@"2",@"3"];
NSLog(@"%@",[person valueForKey:@"names"]);
**********************
// ✅ 输出
2020-02-16 13:04:10.290894+0800 KVC简用[10284:112668] (
    1,
    2,
    3
)

第三点:

// ✅ YXPerson.h
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, strong) NSSet   *set;
**********************
// ✅ YXPerson.m
// 个数
- (NSUInteger)countOfBooks{
    NSLog(@"%s",__func__);
    return [self.set count];
}

// 是否包含这个成员对象
- (id)memberOfBooks:(id)object {
    NSLog(@"%s",__func__);
    return [self.set containsObject:object] ? object : nil;
}

// 迭代器
- (id)enumeratorOfBooks {
    // objectEnumerator
    NSLog(@"来了 迭代编译");
    return [self.array reverseObjectEnumerator];
}
**********************
// ✅ 取值
person.array = @[@"pen0", @"pen1", @"pen2", @"pen3"];
// set 集合
person.set = [NSSet setWithArray:person.array];
NSSet *set = [person valueForKey:@"books"];
[set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"set遍历 %@",obj);
    }];
**********************
// ✅ 输出
2020-02-16 13:14:56.213050+0800 KVC简用[11040:122217] -[YXPerson countOfBooks]
2020-02-16 13:14:56.213228+0800 KVC简用[11040:122217] -[YXPerson countOfBooks]
2020-02-16 13:14:56.213361+0800 KVC简用[11040:122217] 来了 迭代编译
2020-02-16 13:14:56.213494+0800 KVC简用[11040:122217] set遍历 pen3
2020-02-16 13:14:56.213610+0800 KVC简用[11040:122217] set遍历 pen2
2020-02-16 13:14:56.213720+0800 KVC简用[11040:122217] set遍历 pen1
2020-02-16 13:14:56.213825+0800 KVC简用[11040:122217] set遍历 pen0
5.2、setter方法查找

setValue:forKey:的默认实现(给定键和值参数作为输入),尝试将名为key的属性设置为value,在使用这个方法设置值时,对象的内部会经历以下流程。

// ✅ YXPerson.h
@property (nonatomic, copy) NSString *name;
**********************
// ✅ YXPerson.m
//MARK: - setKey. 的流程分析
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
**********************
// ✅ 取值
[person setValue:@"YX" forKey:@"name"];
**********************
// ✅ 输出
2020-02-16 13:21:58.807533+0800 KVC简用[11508:128377] -[YXPerson setName:] - YX
2020-02-16 13:21:58.807533+0800 KVC简用[11508:128377] -[YXPerson _setName:] - YX
2020-02-16 13:21:58.807533+0800 KVC简用[11508:128377] -[YXPerson setIsName:] - YX

第二点:

// ✅ YXPerson.h
@interface YXPerson : NSObject{
    @public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
**********************
// ✅ YXPerson.m
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"%@ is Undefined", key);
}
**********************
// ✅ 取值
[person setValue:@"YX" forKey:@"name"];
 NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
 NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
 NSLog(@"%@-%@",person->name,person->isName);
 NSLog(@"%@",person->isName);
**********************
// ✅ 1. 四个成员变量_name,_isName,name,isName
2020-02-16 13:33:03.731309+0800 KVC简用[12265:138431] YX-(null)-(null)-(null)
2020-02-16 13:33:03.731481+0800 KVC简用[12265:138431] (null)-(null)-(null)
2020-02-16 13:33:03.731596+0800 KVC简用[12265:138431] (null)-(null)
2020-02-16 13:33:03.731693+0800 KVC简用[12265:138431] (null)
// ✅ 2. 三个成员变量_isName,name,isName
2020-02-16 13:35:21.183230+0800 KVC简用[12445:141103] YX-(null)-(null)
2020-02-16 13:35:21.183438+0800 KVC简用[12445:141103] (null)-(null)
2020-02-16 13:35:21.183590+0800 KVC简用[12445:141103] (null)
// ✅ 3. 两个成员变量name,isName
2020-02-16 13:36:00.207210+0800 KVC简用[12502:141939] YX-(null)
2020-02-16 13:36:00.207396+0800 KVC简用[12502:141939] (null)
// ✅ 4. 一个成员变量isName
2020-02-16 13:36:51.657983+0800 KVC简用[12575:142914] YX
**********************
// ✅ 四个成员变量都不在
2020-02-16 13:37:58.726567+0800 KVC简用[12670:144203] name is Undefined

参考:

KVC底层原理

上一篇下一篇

猜你喜欢

热点阅读