目录
- 1. 什么是KVC?
- 2. 访问对象属性
- 常用方法声明
- 基础使用
- KeyPath路径
- 多值操作
- 3. 访问集合属性
- 4. 集合运算符
- 自定义集合运算符
- 5. 非对象值处理
- 访问基本数据类型
- 访问结构体
- 6. 属性验证
- 7. 设值和取值原理
- 基本的Getter搜索模式
- 基本的Setter搜索模式
- NSMutableArray搜索模式
- 其他搜索模式
- 8. 异常处理
- 9. KVC 经典问题
- 通过KVC修改属性会触发KVO吗?
- 通过KVC键值编码技术是否会破坏面向对象的编程方法,或者说违背面向对象的编程思想呢?
- 参考文章
1. 什么是KVC?
KVC的全称是Key-Value Coding,即键值编码,可通过一个key来访问某个属性
KVC是由@interface NSObject(NSKeyValueCoding)
非正式协议启用的一种机制,遵循了这个协议的对象除了直接通过存取方法和点语法来访问属性,也可用KVC来间接访问属性,通过字符串来访问一个对象的成员变量或其关联的存取方法
某些情况下,KVC
还可以帮助简化代码
2. 访问对象属性
常用方法声明
- (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 来赋值
基础使用
以下是Person类的声明:
@interface Person : NSObject {
@private
int _age;
}
//@property (nonatomic, copy, readonly) NSString* name;
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign)int age;
@property (nonatomic, strong)Dog* dog;
@end
对于Person类的实例对象person,可用setter/getter方法对属性进行访问:
// person.name = @"Jaxon";
// NSLog(@"%@", person.name);
[person setName: @"Jaxon"];
NSLog(@"%@", [person name]);
也可通过KVC间接访问,通过key设置或访问其值:
[person setValue: @"Jakey" forKey: @"name"];
NSLog(@"%@", [person valueForKey: @"name"]);
甚至将属性设置为 readonly
只读或私有变量后,也可以通过KVC进行访问:
@interface Person : NSObject {
@private
int _age;
}
@property (nonatomic, copy, readonly) NSString* name;
//@property (nonatomic, assign)int age;
@end
// main函数
[person setValue: @"Jakey" forKey: @"name"];
NSLog(@"%@", [person valueForKey: @"name"]);
//私有变量也可通过KVC进行访问
[person setValue: @11 forKey: @"age"];
NSLog(@"%@", [person valueForKey: @"age"]);
设置变量时,会自动将设置的字符串转换成我们设置的类型@(11)
:
[person setValue: @"11" forKey: @"age"];
KeyPath路径
KVC还支持多级访问,比如我们想访问person
的dog
属性的name
属性的lastName
属性:
[person valueForKeyPath: @"dog.name.lastName"];
keyPath
是一个以点分隔开来的字符串,表示了要遍历的对象属性序列。序列中第一个key相对于接收者,而后续的每个key都与前一级key相关联。keyPath
对于单个方法调用来深入对象内部结构来说很有用
多值操作
给定一组相对于调用者的key存入数组中,该方法会为数组中的每个key调用valueForKay:
方法,将数组中所有key的value以字典的形式返回:
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
/*
NSMutableDictionary <NSString *, id>* dict = [NSMutableDictionary dictionary];
for (NSString* key in keys) {
id value = [self valueForKey: key];
if (value) {
dict[key] = value;
} else {
dict[key] = [NSNull null];
}
}
*/
将指定字典中的value设置到调用者的属性中,默认实现是对每一个键值对调用setValue:ForKey:
方法,设置时需要将nil
替换成NSNull
:
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
/*
NSArray* keyArray = dict.allKeys;
for (NSString* key in keyArray) {
id value = dict[key];
//使用KVC时,key值与属性名一致
if (value) {
[self setValue: value forKey: key];
} else {
[self setValue: [NSNull null] forKey: key];
}
}
*/
3. 访问集合属性
获取或设置集合对象时(主要指NSArray
和NSSet
)仍可通过上述方法对对象进行访问,但是对于操作集合对象内部的元素来说,比如添加或者删除元素,更高效的方式是使用KVC的可变代理方法获取集合代理对象:
// 比如我们为person添加一个不可变array属性
NSArray* array = @[@1, @2, @3];
NSArray* tempArray = @[@0, @1, @3];
person.array = array;
[person setValue: tempArray forKey: @"array"];
NSLog(@"%@", [person valueForKey: @"array"]); // 0 1 3
NSMutableArray* mutableArray = [person mutableArrayValueForKey: @"array"];
mutableArray[0] = @-1;
mutableArray[2] = @-2;
NSLog(@"%@", [person valueForKey: @"array"]); // -1 1 -2
这里用到的mutableArrayValueForKey:
实例方法会通过传入的key返回对应的属性的一个可变数组的代理对象,KVC提供了三种不同的代理对象访问方法,每种都有Key和KeyPath方法:
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
mutableSetValueForKey:
和mutableSetValueForKeyPath:
mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
由于KVO的实现原理是在Runtime运行时生成子类并重写setter方法来达到可以通知所有观察者对象的目的,所以对集合对象进行操作不会触发KVO方法
要使用KVO
监听集合对象的变化时,需通过KVC的可变代理方法获取集合代理对象,然后对代理对象进行操作,当代理对象的内部发生改变时,就会触发KVO方法:
person.array = @[@3, @7, @9]; // 不会触发KVO
Observer* observer = [[Observer alloc] init];
[person addObserver: observer forKeyPath: @"array" options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context: NULL];
NSMutableArray* mutableArray = [person mutableArrayValueForKey: @"array"];
mutableArray[0] = @-1; // 触发KVO
mutableArray[2] = @-2; // 触发KVO
NSLog(@"%@", [person valueForKey: @"array"]);
监听方法中打印change参数,可以看到还将改变的索引值展示了出来:
4. 集合运算符
KVC的valueForKeyPath:
除了可以多级访问外,还可以使用集合运算符来实现一些高效的运算操作
以下是集合运算符的结构:
- Left Key Path:左键路径,指向要进行运算的集合,如果调用者就是集合对象,则Left部分可以省略
- Collection Operator:集合运算符,是一小部分关键字其后带有一个
@
符号,该符号指定getter
在返回数据之前以某种方式处理数据应执行的操作 - Right Key Path:右键路径,要进行运算操作的集合中的属性,除了
@count
运算符外,所有的集合运算符的Right部分不能省略
而集合运算符主要分为三类:
-
聚合运算符:以某种方式合并集合中的对象,并返回右键路径中指定属性的数据类型匹配的一个对象,一般返回
NSNumber
对象@count
:返回操作对象指定属性的个数@max
:返回操作对象指定属性的最大值@min
:返回操作对象指定属性的最小值@sum
:返回操作对象指定属性值之和@avg
:返回操作对象指定属性的平均值
比如给Person类添加一数组属性,数组元素为Book类,而Book类有一price属性:
@interface Person : NSObject // ... @property (nonatomic, strong)NSArray <Book *>* books; //每个人都有几本书,且每本书的价格不同 @end // main函数 NSMutableArray* bookArray = [NSMutableArray array]; for (int i = 1; i < 100; ++i) { Book* book = [[Book alloc] init]; book.price = i; [bookArray addObject: book]; } personWithBooks.books = bookArray; NSNumber* countOfBooks = [personWithBooks valueForKeyPath: @"books.@count"]; NSLog(@"%@", countOfBooks); NSNumber* maxPrice = [personWithBooks valueForKeyPath: @"books.@max.price"]; NSNumber* minPrice = [personWithBooks valueForKeyPath: @"books.@min.price"]; NSNumber* averPrice = [personWithBooks valueForKeyPath: @"books.@avg.price"]; NSNumber* sumPrice = [personWithBooks valueForKeyPath: @"books.@sum.price"]; NSLog(@"maxPrice: %@ minPrice: %@ averPrive: %@ sumPrice: %@ = %@", maxPrice, minPrice, averPrice,sumPrice, @(99 * 100 / 2));
来看打印结果:
备注:
@max
和@min
根据右键路径指定的属性在集合中搜索,搜索使用compare:
方法进行比较,许多基础类 (如NSNumber类) 中都有定义。因此,右键路径指定的属性必须能响应compare:
消息。搜索忽略值为nil
的集合项。可以通过重写compare:
方法对搜索过程进行控制。 -
数组操作符:根据运算符的条件,将符合条件的对象以一个
NSArray
实例返回@unionOfObjects
:返回操作对象指定属性的集合@distinctUnionOfObjects
: 返回操作对象指定属性的集合–去重
NSArray* priceArr = @[@5, @4, @6, @4, @4, @75, @245, @35, @6]; NSMutableArray* bookArr = [NSMutableArray array]; for (int i = 0; i < priceArr.count; ++i) { Book* book = [[Book alloc] init]; book.price = [priceArr[i] doubleValue]; [bookArr addObject: book]; } // 获取集合中所有元素的price NSArray* returnArr = [bookArr valueForKeyPath: @"@unionOfObjects.price"]; NSLog(@"%@", returnArr); // 获取集合中所有元素不同price NSArray* returnDistinctArr = [bookArr valueForKeyPath: @"@distinctUnionOfObjects.price"]; NSLog(@"%@", returnDistinctArr);
运行结果:
注意: 在使用数组运算符时,如果有任何操作的对象为
nil
,则valueFoeKeyPath:
方法将引发异常 -
嵌套运算符:处理集合对象中嵌套其他集合对象的情况,并根据运算符返回一个
NSArray
或NSSet
实例-
@unionOfArrays
:读取集合中的每个集合中的每个元素的右键路径指定的属性,放在一个NSArray
实例中并返回 -
@distinctUnionOfArrays
:读取集合中的每个集合中的每个元素的右键路径指定的属性,放在一个NSArray
实例中,将数组进行去重后返回 -
@distinctUnionOfSets
:读取集合中的每个集合中的每个元素的右键路径指定的属性,放在一个NSSet
实例中,去重后返回- 在使用嵌套运算符时,valueForKeyPath:内部会根据运算符创建一个NSMutableArray或NSMutableSet对象,将集合中的array和set添加进去再进行操作。如果集合中有非集合元素,会导致Crash
- 使用unionOfArrays或distinctUnionOfArrays运算符,消息接收者应该是arrayOfArrays类型,即
NSArray< NSArray* >* arrayOfArrays;
;使用distinctUnionOfSets运算符,消息接收者应该是setOfSets
或者arrayOfSets
类型,否则会发生异常 - 在使用嵌套运算符时,如果有任何操作的对象为nil, 则valueForKeyPath:方法将引发异常
-
-
如果集合中的对象都是
NSNumber
,右键路径可以用self
NSArray* priceArr = @[@5, @4, @6, @4, @4, @75, @245, @35, @6]; NSNumber* sum = [priceArr valueForKeyPath: @"@sum.self"]; NSLog(@"%@", sum);
自定义集合运算符
上面介绍了KVC为我们提供的集合运算符,我们能不能自定义呢?
我们使用Runtime中的函数打印NSArray类的方法列表:
u_int count; // unsigned int
Method* methods = class_copyMethodList([NSArray class], &count);
for (int i = 0; i < count; ++i) {
Method method = methods[i];
SEL sel = method_getName(method);
NSLog(@"%d --- %@", i, NSStringFromSelector(sel));
}
free(methods);
方法很多,其中也发现了KVC提供的集合运算符都有对应的方法_<operatorKey>ForKeyPath:
:
再来看一下NSSet类支持哪些集合运算符:
可见NSSet类不支持@unionOfObjects
和@unionOfArrays
运算符,如果使用了就会抛出异常NSInvalidArgumentException并导致程序崩溃,reason: [<__NSSetI 0x6000017a12f0> valueForKeyPath:]: this class does not implement the unionOfArrays operation.不支持该运算符
而NSArray类虽然支持
@distinctUnionOfSets
运算符,但其必须是arrayOfSets类型,即NSArray< NSSet* >* arrayOfSets;
。因为_distinctUnionOfSetsForKeyPath方法中会创建一个NSMutableSet实例,并调用unionSet:方法将集合中的set的元素添加进去再进行操作。如果是arrayOfArrays类型就会抛出异常NSInvalidArgumentException并导致程序崩溃,reason: '*** -[NSMutableSet unionSet:]: set argument is not an NSSet’即集合中有非NSSet元素。
我们模仿以上方法使用分类给NSArray动态添加一个实现集合运算符@medium
的方法:
// 4 4 5 35 75 245
NSArray* array = @[@5, @4, @4, @75, @245, @35];
NSNumber* num = [array valueForKeyPath: @"@medium.self"];
NSLog(@"%@", num);
此方法用于获取数组中的中位数,打印结果为:20
5. 非对象值处理
KVC还支持对非对象属性进行访问
非对象类型分为两类:基本数据类型(标量)、结构体
- 当非对象类型作为参数传入
valueForKey:
时,会使用该值初始化一个NSNumber
(用于基础数据类型)或NSValue
(用于结构体)实例返回 - 当非对象类型作为参数传入
setValueForKey:
时,会发送一条<type>Value
消息给value对象以提取基础数据,而后赋值给key
注意:
- 因为Swift中的所有属性都是对象,所以这里仅适用于Objective-C属性。
- 当进行赋值如setValue:forKey:时,如果key的数据类型是非对象类型,则value就禁止传nil"。否则会调用setNilValueForKey:方法,该方法的默认实现抛出异常NSInvalidArgumentException,并导致程序Crash。
访问基本数据类型
访问结构体
除上述结构体外,对于自定义的结构体,也需要进行包装成NSValue
:
typedef struct {
float x, y, z;
} ThreeFloats;
@interface Man : NSObject
@property (nonatomic)ThreeFloats threeFloats;
@end
// 取值
NSValue* result = [myClass valueForKey: @"threeFloats"];
// 赋值
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes: &floats objCType: @encode(ThreeFloats)];
[man setValue: value forKeyPath: @"threeFloats"];
6. 属性验证
KVC支持属性验证,可以在使用KVC赋值前验证能否为这个key赋值指定value
实现方法:
- (BOOL)validateValue:(id _Nullable *)value forKey:(NSString *)key error:(NSError * _Nullable *)error;
- (BOOL)validateValue:(inout id _Nullable *)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError * _Nullable *)outError;
这个验证方法的默认实现是查看 消息接收者(或keyPath中最后的对象) 中是否实现了遵循命名规则为validate<Key>:error:
的方法,如果没有,验证默认成功,返回YES
。而由于validate<Key>:error:
通过引用接收值和错误参数,所以会有以下三种结果:
- 验证成功,返回
YES
,对属性值不做任何改动 - 验证失败,返回
NO
,但对属性值不做改动,如果调用者提供了NSError的话,就把错误引用设置为指示错误原因的NSError对象 - 验证失败,返回 YES,创建一个新的,有效的属性值作为替代。在返回之前,该方法将值引用修改为指向新值对象。 进行修改时,即使值对象是可变的,该方法也总是创建一个新对象,而不是修改旧对象
可以在消息接收者类中实现validate<Key>:error:
的方法来自定义逻辑返回YES
或NO
,在Person类中实现validateName:error:
方法,验证给name
赋的值是不是Jaxon
:
Person *person = [[Person alloc] init];
NSString *value = @"Rose";
NSString *key = @"name";
NSError *error;
BOOL result = [person validateValue: &value forKey: key error: &error];
if (error) {
NSLog(@"error = %@", error);
return;
}
NSLog(@"%d",result);
// Person.m
// 按照上面的逻辑,会根据key,即@"name",调用一下方法
- (BOOL)validateName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError {
NSString *name = *value;
BOOL result = NO;
if ([name isEqualToString:@"Jaxon"]) {
result = YES;
}
return result;
}
// 打印:0
KVC或其默认实现均未定义任何机制来自动的执行属性验证,也就是说需要在适合你的应用的时候自己提供属性验证方法
某些其他 Cocoa 技术在某些情况下会自动执行验证。 例如,保存 managed object context 时,Core Data会自动执行验证。另外,在 macOS 中,Cocoa Binding允许你指定验证应自动进行
7. 设值和取值原理
基本的Getter搜索模式
valueForKey:
方法会在调用者传入key之后在对象中按下列步骤进行模式搜索:
- 按照
get<Key>
、<key>
、is<Key>
、_<key>
的顺序查找对象中是否有对应的方法
如果找到就调用取值✅并执行5.,否则执行2. - 按照
countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
顺序查找方法(对应于NSArray类定义的原始方法)- 如果找到上述第一个方法,再找到其他两个中的至少一个,则创建一个响应所有NSArray方法的代理集合对象,并返回该对象(即要么
countOf<Key> + objectIn<Key>AtIndex:
、要么countOf<Key> + <key>AtIndexes:
或者countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:
三种组合)✅- 代理对象随后将其接收到的任何NSArray消息转换为
countOf<Key>
、objectIn<Key>AtIndex:
、<Key>AtIndexes:
消息的组合,并将其发送给KVC调用方。如果原始对象还实现了一个名为get<Key>:range:
的可选方法,则代理对象也会在适当时使用该方法 - 当KVC调用方与代理对象一起工作时,允许底层属性的行为如同NSArray一样,即使它不是NSArray
- 代理对象随后将其接收到的任何NSArray消息转换为
- 如果没有找到,跳转至3.
- 如果找到上述第一个方法,再找到其他两个中的至少一个,则创建一个响应所有NSArray方法的代理集合对象,并返回该对象(即要么
- 按照
countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
顺序查找方法(对应于NSSet类定义的原始方法)- 如果三个方法都找到,则创建一个响应所有NSSet方法的代理集合对象,并返回该对象✅
- 代理对象随后将其接收到的任何NSSet消息转换为
countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
消息的组合,并将其发送给KVC调用方 - 当KVC调用方与代理对象一起工作时,允许底层属性的行为如同NSSet一样,即使它不是NSSet
- 代理对象随后将其接收到的任何NSSet消息转换为
- 如果没有找到,跳转至4.
- 如果三个方法都找到,则创建一个响应所有NSSet方法的代理集合对象,并返回该对象✅
- 查看消息接收者类的
+ (BOOL)accessInstanceVariablesDirectly;
方法返回值- 如果返回YES,就按照
_<key>
、_is<Key>
、<key>
、is<Key>
顺序查找成员变量,如果找到就直接取值✅并执行5.,否则执行6. - 如果返回NO,跳转到6.
- 如果返回YES,就按照
- 判断取出的属性值✅:
- 如果属性值是对象,直接返回
- 如果属性值不是对象,但是可以转化为NSNumber类型,则将属性值转化为NSNumber类型返回
- 如果属性值不是对象,也不能转化为NSNumber类型,则将属性值转化为NSValue类型返回
- 调用
valueForUndefinedKey:
方法,该方法抛出异常NSUnknownKeyException,并导致程序Crash,这是默认实现,我们可以重写该方法根据特定key
做一些特殊处理
基本的Setter搜索模式
setValue:forKey:
方法默认实现会在调用者传入key
和value
(如果是非对象类型,则指的是解包之后的值) 之后会在对象中按下列的步骤进行模式搜索:
- 按照
set<Key>:
、_set<Key>:
顺序查找方法
如果找到就将value传进(根据需要进行转换)方法里并调用✅,否则执行2. - 查看消息接收者类的
+ (BOOL)accessInstanceVariablesDirectly;
方法返回值(默认返回YES)- 如果返回YES,就按照
_<key>
、_is<Key>
、<key>
、is<Key>
顺序查找成员变量(同Getter搜索模式),如果找到就将value传进方法✅并执行,否则执行3. - 如果返回NO,跳转到3.
- 如果返回YES,就按照
- 调用
setValueForUndefinedKey:
方法,该方法抛出异常NSUnknownKeyException,并导致程序Crash,这是默认实现,我们可以重写该方法根据特定key
做一些特殊处理
NSMutableArray搜索模式
mutableArrayValueForKey:
方法的默认实现,给定一个key作为输入参数,返回属性名为key的集合的代理对象(NSMutableArray),在消息接收者中操作,执行以下搜索步骤:
-
查找一对方法
insertObject:in<Key>AtIndex:
、removeObjectFrom<Key>AtIndex:
(相当于NSMutableArray的原始方法insertObject:atIndex:
和removeObjectAtIndex:
)或者insert<Key>:atIndexes:
、remove<Key>AtIndexes:
(相当于NSMutableArray的原始方法insertObjects:atIndexes:
和removeObjectsAtIndexes:
)-
如果至少实现了一个
insert
方法和一个remove
方法,则返回一个代理对象✅,来响应发送给NSMutableArray的消息,通过发送insertObject:in<Key>AtIndex:
、removeObjectFrom<Key>AtIndex:
、insert<Key>:atIndexes:
、remove<Key>AtIndexes:
组合消息给KVC调用方,否则执行2.该代理对象类型为NSKeyValueFastMutableArray2,继承链为NSKeyValueFastMutableArray2->NSKeyValueFastMutableArray->NSKeyValueMutableArray->NSMutableArray
-
如果我们也实现了一个可选的
replace object
方法,如replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
,代理对象在适当的情况下也会使用它们,以获得最佳性能
-
-
查找
set<Key>:
方法
如果找到,就会向KVC调用方发送一个set<Key>:
消息,来返回一个响应NSMutableArray消息的代理对象✅,否则执行3.该代理对象类型为NSKeyValueSlowMutableArray,继承链为NSKeyValueSlowMutableArray->NSKeyValueMutableArray->NSMutableArray
注意:此步骤中描述的机制比上一步的效率低得多,因为它可能重复创建新的集合对象,而不是修改现有的集合对象。因此,在设计自己的键值编码兼容对象时,通常应该避免使用它。给代理对象发送NSMutableArray消息都会调用
set<Key>:
方法。即对代理对象进行修改,都是调用set<Key>:
来重新赋值,所以效率会低很多 -
查看消息接收者类的
+ (BOOL)accessInstanceVariablesDirectly;
方法返回值(默认返回YES)- 如果返回YES,就按照
_<key>
、<key>
顺序查找成员变量。如果找到就返回一个代理对象✅,该代理对象将接收所有NSMutableArray消息,通常是NSMutableArray或其子类,否则执行4. - 如果返回NO,执行4.
- 如果返回YES,就按照
-
返回一个可变的集合代理对象。当它接收到NSMutableArray消息时,发送一个valueForUndefinedKey:消息给KVC调用方,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定
key
做一些特殊处理
其他搜索模式
除以上三种搜索模式,KVC还有NSMutableSet
和NSMutableOrderedSet
两种搜索模式,它们的搜索规则和NSMutableArray相同,只是搜索和调用的方法不同
8. 异常处理
根据上述KVC搜索规则,当没有搜索到对应的key或者keyPath相关方法或者变量时,会调用以下方法:
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
这两个方法的默认实现是抛出异常NSUnknownKeyException
,并导致程序Crash:
我们可以通过重写这两个方法来处理异常:
-
当我们用
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]; } }
-
比如从服务器上给我回传了一个人Person,而这个人往往都是有编号的id,得到的字段往往是小写的“id”。意味着我们Model中的属性名也需要是小写的id,但在OC中id是关键字,不能用作属性名,那我们无法接收了吗。解决方案是利用KVC将
id
转换成大写的ID
:// 由于实际是没有名为id的属性的,所以会调用以下方法 - (void)setValue:(id)value forUndefinedKey:(NSString *)key { if ([key isEqualToString: @"id"]) { self.ID = value; } } - (id)valueForUndefinedKey:(NSString *)key { return self.ID; }
这样重写可帮助我们躲避一些关键字(如
id
):[person setValue: @"1001" forKey: @"id"]; NSLog(@"%@", [person valueForKey: @"id"]);
一般服务器给我们返回的是一个字典(JSON数据),需将其转化成Person模型:
NSDictionary* dict = @{ @"name" : @"Jaxon", @"age" : @"20", @"id" : @"1001" }; //dict -> model 字典转换模型 Person* p = [[Person alloc] initWithDictionary3: dict]; NSLog(@"name = %@ age = %d id = %@", p.name, p.age, p.ID); //model -> dict 模型转换字典 NSArray* array = @[@"name", @"age", @"id"]; NSDictionary* dict1 = [p dictionaryWithValuesForKeys: array]; NSLog(@"%@", dict1);
以下是转换模型方法的实现:
- (instancetype)initWithDictionary: (NSDictionary *)dict {
self = [super init];
if (self) {
_age = [dict[@"age"] intValue];
_name = dict[@"name"];
_ID = dict[@"id"];
}
return self;
}
//Person的信息太多的话,一一赋值就显得冗杂
- (instancetype)initWithDictionary2: (NSDictionary *)dict {
self = [super init];
if (self) {
NSArray* keyArray = dict.allKeys;
for (NSString* key in keyArray) {
id value = dict[key];
//使用KVC时,key值与属性名一致
if (value) {
[self setValue: value forKey: key];
} else {
[self setValue: [NSNull null] forKey: key];
}
}
}
return self;
}
- (instancetype)initWithDictionary3: (NSDictionary *)dict {
self = [super init];
if (self) {
[self setValuesForKeysWithDictionary: dict];
}
return self;
}
9. KVC 经典问题
通过KVC修改属性会触发KVO吗?
按照KVC的搜索模式,会调用到属性的setter方法,所以若是该属性被监听,值被更改后一定会出发监听方法
值得注意的是,通过 KVC,实际就算直接修改成员变量,也会触发 KVC 监听方法
通过KVC键值编码技术是否会破坏面向对象的编程方法,或者说违背面向对象的编程思想呢?
valueForKey:和setValue:forKey:这里面的key是没有任何限制的,当我们知道一个类或实例它内部的私有变量名称的情况下,我们在外界可以通过已知的key来对它的私有变量进行访问或者赋值的操作,从这个角度来讲KVC键值编码技术会违背面向对象的编程思想
参考文章
KVC - Accessor Search Patterns
Key-Value Coding Programming Guide(苹果官方文档)