[iOS] KVC 学习
[iOS] KVC 学习
文章目录
- [iOS] KVC 学习
- 前言
- KVC
- 定义
- KVC 取值以及怎么寻找 Key
- KVC 取值
- KVC使用keyPath
- KVC处理异常
- KVC的应用
前言
本篇博客主要介绍 KVC 的相关内容。
KVC
定义
KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。
KVC 的定义是通过 NSObject 的拓展来实现的,下面是关于 KVC 的四个重要方法
- (nullable id)valueForKey:(NSString *)key; //直接通过属性名来取值- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过属性名来设值- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过属性路径来取值- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过属性路径来设值
KVC 取值以及怎么寻找 Key
当调用 [object setValue:someValue forKey:@"someKey"];
时,KVC 会按照一个明确且有序的规则来查找并设置属性值。这个过程可以分为以下几个步骤:
- 查找
set<Key>:
或_set<Key>:
方法- 首先,程序会查找名为
setSomeKey:
或_setSomeKey:
的方法(其中 “someKey” 的首字母大写)。 - 如果找到了这两个方法中的任意一个,系统会直接调用该方法,将
someValue
作为参数传入,整个设值流程结束。这是最优先、也是最常见的设值方式。
- 首先,程序会查找名为
- 检查
accessInstanceVariablesDirectly
- 如果第一步没有找到任何相关的 setter 方法,KVC 会调用类方法
+ (BOOL)accessInstanceVariablesDirectly
来询问是否允许直接访问成员变量。 - 该方法的默认返回值是
YES
。如果您的类重写了这个方法并返回NO
,那么查找过程会在此处停止,并直接进入第四步(调用setValue:forUndefinedKey:
)。
- 如果第一步没有找到任何相关的 setter 方法,KVC 会调用类方法
- 直接访问成员变量(当
accessInstanceVariablesDirectly
返回YES
时)- 如果允许直接访问,KVC 会按照以下顺序在类中查找与 key 匹配的成员变量(Instance Variable, ivar):
_<key>
(例如_someKey
)_is<Key>
(例如_isSomeKey
)<key>
(例如someKey
)is<Key>
(例如isSomeKey
)
- 一旦找到其中任何一个成员变量,KVC 就会直接将
someValue
赋给这个成员变量,流程结束。
- 如果允许直接访问,KVC 会按照以下顺序在类中查找与 key 匹配的成员变量(Instance Variable, ivar):
- 调用
setValue:forUndefinedKey:
- 如果以上所有步骤都没有找到任何可用的 setter 方法或成员变量,系统会调用
setValue:forUndefinedKey:
方法。 NSObject
中该方法的默认实现是抛出一个NSUnknownKeyException
异常,这通常会导致程序崩溃。这也就是您之前遇到的this class is not key value coding-compliant for the key
错误的直接原因。- 您可以重写这个方法来自定义错误处理逻辑,避免程序崩溃,例如可以记录一个错误日志,或者什么都不做。
- 如果以上所有步骤都没有找到任何可用的 setter 方法或成员变量,系统会调用
在下面我给出一个完整的例子来解释这个过程
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface KVC1 : NSObject
{@publicNSString *isAge;NSString *_age;
}
@property NSString *str;
@property NSArray *ary;
@endNS_ASSUME_NONNULL_END
#import "KVC1.h"@implementation KVC1+ (BOOL)accessInstanceVariablesDirectly {return NO;
}
- (id)valueForUndefinedKey:(NSString *)key {NSLog(@"出现异常");return nil;
}
- (void)setValue:(id)value forKey:(NSString *)key {NSLog(@"出现异常,无法设置");
}@end
#import <Foundation/Foundation.h>
#import "KVC1.h"
#import "KVCNext.h"
int main(int argc, const char * argv[]) {@autoreleasepool {KVC1 *test = [[KVC1 alloc] init];[test setValue:@[] forKey:@"ary"];[test setValue:@"12222" forKey:@"str"];[test setValue:@"12" forKey:@"age"];NSLog(@"%@", test->isAge);NSLog(@"%@", test->_age);NSLog(@"%@", test.str);NSLog(@"%@", test.ary);NSLog(@"%@", [test valueForKey:@"age"]);}return 0;
}
在这里我们在.m文件中写的这几个方法就是阻止我们使用kvc的方法,首先我们先重写accessInstanceVariablesDirectly
方法让其返回NO,再运行代码(注意上面注释的部分),Xcode直接打印出
这说明了重写+(BOOL)accessInstanceVariablesDirectly
方法让其返回NO后,KVC找不到SetName:方法后,不再去找name系列成员变量,而是直接调用forUndefinedKey方法,所以开发者如果不想让自己的类实现KVC,就可以这么做。
下面我们把.m 文件中的三个方法都注释后,来解说他的一个智能搜索逻辑。
我们来看一下.h 的成员变量部分
@interface KVC1 : NSObject
{@publicNSString *isAge;NSString *_age;
}
@property NSString *str;
@property NSArray *ary;
@end
还有主函数部分
#import <Foundation/Foundation.h>
#import "KVC1.h"
#import "KVCNext.h"
int main(int argc, const char * argv[]) {@autoreleasepool {KVC1 *test = [[KVC1 alloc] init];[test setValue:@[] forKey:@"ary"];[test setValue:@"12222" forKey:@"str"];[test setValue:@"12" forKey:@"age"];NSLog(@"%@", test->isAge);NSLog(@"%@", test->_age);NSLog(@"%@", test.str);NSLog(@"%@", test.ary);NSLog(@"%@", [test valueForKey:@"age"]);}return 0;
}
下面是输出的结果
这时可以发现我们 [test setValue:@"12" forKey:@"age"];
是这么赋值的但是NSLog(@"%@", test->_age);
读取的时候是在 age
前面加了下横杠的这个并不是什么巧合这就是 kvc 的智能搜索,如果此时我们注释掉 age 这个成员变量,我们会发现另一个神奇的现象
isAge 被赋值了,我们在使用 valueForKey 的方法读取值的时候使用的 age 我们同样也能读到 isAge 的值。
这是就像我们在前面说到的KVC 会以极其灵活的方式,按顺序查找以下四种格式的方法名:
get<Key>
<key>
is<Key>
_<key>
就比如 调用
[person valueForKey:@"name"]
KVC 会依次查找getName
、name
、isName
、_name
这四个方法。只要找到其中任何一个,就会立即调用并返回结果,搜索结束。
大家可以在自己的 Xcode 上尝试一下。
KVC 取值
有关于 KVC 取值,KVC 会以非常灵活的方式,按顺序查找以下四种命名格式的 Getter 方法:
get<Key>
(例如,key 为 “name” 时,查找getName
)<key>
(例如,查找name
)is<Key>
(例如,查找isName
,常用于布尔值)_<key>
(例如,查找_name
)
一般情况就是我们按照这三种顺序进行查找
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface KVC1 : NSObject
{@publicNSString *isAge;NSString *_age;
}
@property NSString *str;
@property NSArray *ary;
@endNS_ASSUME_NONNULL_END
#import "KVC1.h"@implementation KVC1
- (int) getAge {return 1222;
}
- (int) age {return 120;
}
- (int) isAge {return 10;
}
@end
#import <Foundation/Foundation.h>
#import "KVC1.h"
#import "KVCNext.h"
int main(int argc, const char * argv[]) {@autoreleasepool {KVC1 *test = [[KVC1 alloc] init];[test setValue:@[] forKey:@"ary"];[test setValue:@"12222" forKey:@"str"];[test setValue:@"12" forKey:@"age"];}return 0;
}
下面我给出打印的结果
这里返回的是我们getAge的方法返回的函数。
这时我们把getAge注释,再次运行
我们再把age注释,再次运行
最后注释掉isAge可以得到
KVC使用keyPath
keyPath
是 Objective-C 中一个非常强大和重要的概念。它通过一个简单的字符串路径,能够动态、深入地访问和操作对象的数据,是 KVC 和 KVO 这两大核心技术的基础。他的最重要的作用就是可以在监听自定义和复杂的数据类型的时候可以简化代码量。
如下的代码就是 keyPath的实际应用
#import <Foundation/Foundation.h>
#import "KVC1.h"
NS_ASSUME_NONNULL_BEGIN@interface KVCNext : NSObject
@property KVC1 *test;
@endNS_ASSUME_NONNULL_END
#import "KVCNext.h"@implementation KVCNext@end
#import <Foundation/Foundation.h>
#import "KVC1.h"
#import "KVCNext.h"
int main(int argc, const char * argv[]) {@autoreleasepool {KVC1 *test = [[KVC1 alloc] init];KVCNext *test2 = [[KVCNext alloc] init];[test2 setValue:test forKey:@"test"];[test2 setValue:@"1222" forKeyPath:@"test.str"];NSLog(@"%@", [test2 valueForKeyPath:@"test.str"]);}return 0;
}
下面是打印的结果
我们只用通过一个点语法就可以做到修改对应路径的值。
KVC处理异常
在这里其实有两大部分,一部分是读取找不到key和给找不到的key赋值,另一部分是给key赋nil值。
那我们先从第一部分开始
获取值时发生异常 (valueForKey:
)
默认行为:当你调用 [object valueForKey:@"someNonExistentKey"]
时,KVC 首先会查找 someNonExistentKey
对应的访问器方法或实例变量。如果都找不到,它会调用当前对象的 valueForUndefinedKey:
方法。这个方法的默认实现是抛出 NSUnknownKeyException
异常,导致程序崩溃。
如何处理:为了避免崩溃,你可以在你的类中重写 valueForUndefinedKey:
方法,并提供自定义的处理逻辑。
下面是代码演示
#import "KVC1.h"@implementation KVC1
- (id)valueForUndefinedKey:(NSString *)key {NSLog(@"警告:尝试访问一个不存在的 key '%@'", key);return nil;
}
@end
id value = [test valueForKey:@"aRandomKeyThatDoesNotExist"];NSLog(@"获取到的值: %@", value);
下面是输出结果
设置值时发生异常 (setValue:forKey:
)
默认行为:当你调用 [object setValue:someValue forKey:@"someNonExistentKey"]
时,如果找不到 someNonExistentKey
,KVC 会调用 setValue:forUndefinedKey:
方法。它的默认实现也是抛出 NSUnknownKeyException
异常。
如何处理:同样,通过重写 setValue:forUndefinedKey:
方法来捕获这个操作。
下面是代码演示
#import "KVC1.h"@implementation KVC1
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {NSLog(@"警告:尝试为不存在的 key '%@' 设置值 '%@'", key, value);
}
@end
[test setValue:@"some data" forKey:@"anotherRandomKey"];
下面是输出结果
下面是第二部分
处理 nil
值赋给非对象属性
默认行为:假设你有一个属性 age
是 NSInteger
类型。如果你尝试执行 [person setValue:nil forKey:@"age"]
,KVC 不知道如何将 nil
转换成一个标量(非对象)值。因此,它会调用 setNilValueForKey:
方法。这个方法的默认实现是抛出 NSInvalidArgumentException
异常。
如何处理:在你的类中重写 setNilValueForKey:
方法,为标量属性提供一个默认值。
#import "KVC1.h"@implementation KVC1
- (void)setNilValueForKey:(NSString *)key {if ([key isEqualToString:@"age"]) {NSLog(@"警告:不能将 nil 赋值给 age,已设置为默认值 0");self.age = 0;} else {[super setNilValueForKey:key];}
}
@end
[test setValue:nil forKey:@"age"];
下面是输出结果
KVC的应用
NSArray* arrStr = @[@"english",@"franch",@"chinese"];NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];for (NSString* str in arrCapStr) {NSLog(@"%@",str);}NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];for (NSNumber* length in arrCapStrLength) {NSLog(@"%ld",(long)length.integerValue);}NSLog(@"%@", test.ary);
在上述代码中我们实现了一个高阶的消息传递,在这里我们可以注意到,使用 KVC 我们获得的是一个返回后的一个容器,没错在里面他的实现就是把方法传递给每一个元素然后让他重新返回一个容器,实现了一个特殊的效果。
下面是输出的结果