[iOS] KVC 学习

[iOS] KVC 学习

文章目录

前言

本篇博客主要介绍 KVC 的相关内容。

KVC

定义

KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。

KVC 的定义是通过 NSObject 的拓展来实现的,下面是关于 KVC 的四个重要方法

objc 复制代码
- (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 会按照一个明确且有序的规则来查找并设置属性值。这个过程可以分为以下几个步骤:

  1. 查找 set<Key>:_set<Key>: 方法
    • 首先,程序会查找名为 setSomeKey:_setSomeKey: 的方法(其中 "someKey" 的首字母大写)。
    • 如果找到了这两个方法中的任意一个,系统会直接调用该方法,将 someValue 作为参数传入,整个设值流程结束。这是最优先、也是最常见的设值方式。
  2. 检查 accessInstanceVariablesDirectly
    • 如果第一步没有找到任何相关的 setter 方法,KVC 会调用类方法 + (BOOL)accessInstanceVariablesDirectly 来询问是否允许直接访问成员变量。
    • 该方法的默认返回值是 YES。如果您的类重写了这个方法并返回 NO,那么查找过程会在此处停止,并直接进入第四步(调用 setValue:forUndefinedKey:)。
  3. 直接访问成员变量(当 accessInstanceVariablesDirectly 返回 YES 时)
    • 如果允许直接访问,KVC 会按照以下顺序在类中查找与 key 匹配的成员变量(Instance Variable, ivar):
      1. _<key> (例如 _someKey)
      2. _is<Key> (例如 _isSomeKey)
      3. <key> (例如 someKey)
      4. is<Key> (例如 isSomeKey)
    • 一旦找到其中任何一个成员变量,KVC 就会直接将 someValue 赋给这个成员变量,流程结束。
  4. 调用 setValue:forUndefinedKey:
    • 如果以上所有步骤都没有找到任何可用的 setter 方法或成员变量,系统会调用 setValue:forUndefinedKey: 方法。
    • NSObject 中该方法的默认实现是抛出一个 NSUnknownKeyException 异常,这通常会导致程序崩溃。这也就是您之前遇到的 this class is not key value coding-compliant for the key 错误的直接原因。
    • 您可以重写这个方法来自定义错误处理逻辑,避免程序崩溃,例如可以记录一个错误日志,或者什么都不做。

在下面我给出一个完整的例子来解释这个过程

objc 复制代码
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface KVC1 : NSObject
{
    @public
    NSString *isAge;
    NSString *_age;
}
@property NSString *str;
@property NSArray *ary;
@end

NS_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
objc 复制代码
#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 的成员变量部分

objc 复制代码
@interface KVC1 : NSObject
{
    @public
    NSString *isAge;
    NSString *_age;
}
@property NSString *str;
@property NSArray *ary;
@end

还有主函数部分

objc 复制代码
#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 会以极其灵活的方式,按顺序查找以下四种格式的方法名:

  1. get<Key>
  2. <key>
  3. is<Key>
  4. _<key>

就比如 调用 [person valueForKey:@"name"] KVC 会依次查找 getNamenameisName_name 这四个方法。只要找到其中任何一个,就会立即调用并返回结果,搜索结束。

大家可以在自己的 Xcode 上尝试一下。

KVC 取值

有关于 KVC 取值,KVC 会以非常灵活的方式,按顺序查找以下四种命名格式的 Getter 方法:

  1. get<Key> (例如,key 为 "name" 时,查找 getName)
  2. <key> (例如,查找 name)
  3. is<Key> (例如,查找 isName,常用于布尔值)
  4. _<key> (例如,查找 _name)

一般情况就是我们按照这三种顺序进行查找

objc 复制代码
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface KVC1 : NSObject
{
    @public
    NSString *isAge;
    NSString *_age;
}
@property NSString *str;
@property NSArray *ary;
@end

NS_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的实际应用

objc 复制代码
#import <Foundation/Foundation.h>
#import "KVC1.h"
NS_ASSUME_NONNULL_BEGIN

@interface KVCNext : NSObject
@property KVC1 *test;
@end

NS_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: 方法,并提供自定义的处理逻辑。

下面是代码演示

objc 复制代码
#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: 方法来捕获这个操作。

下面是代码演示

objc 复制代码
#import "KVC1.h"

@implementation KVC1
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"警告:尝试为不存在的 key '%@' 设置值 '%@'", key, value);
}
@end
[test setValue:@"some data" forKey:@"anotherRandomKey"];

下面是输出结果

下面是第二部分

处理 nil 值赋给非对象属性

默认行为 :假设你有一个属性 ageNSInteger 类型。如果你尝试执行 [person setValue:nil forKey:@"age"],KVC 不知道如何将 nil 转换成一个标量(非对象)值。因此,它会调用 setNilValueForKey: 方法。这个方法的默认实现是抛出 NSInvalidArgumentException 异常。

如何处理 :在你的类中重写 setNilValueForKey: 方法,为标量属性提供一个默认值。

objc 复制代码
#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的应用

objc 复制代码
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 我们获得的是一个返回后的一个容器,没错在里面他的实现就是把方法传递给每一个元素然后让他重新返回一个容器,实现了一个特殊的效果。

下面是输出的结果

相关推荐
●VON17 小时前
重生之我在大学自学鸿蒙开发第九天-《分布式流转》
学习·华为·云原生·harmonyos·鸿蒙
无妄无望17 小时前
docker学习(4)容器的生命周期与资源控制
java·学习·docker
2501_9159090619 小时前
iOS 混淆实战,多工具组合完成 IPA 混淆与加固(源码 + 成品 + 运维一体化方案)
android·运维·ios·小程序·uni-app·iphone·webview
Larry_Yanan20 小时前
QML学习笔记(四十二)QML的MessageDialog
c++·笔记·qt·学习·ui
能不能别报错20 小时前
K8s学习笔记(十九) K8s资源限制
笔记·学习·kubernetes
十安_数学好题速析21 小时前
倍数关系:最多能选出多少个数
笔记·学习·高考
vue学习21 小时前
docker 学习dockerfile 构建 Nginx 镜像-部署 nginx 静态网
java·学习·docker
Lynnxiaowen1 天前
今天我们开始学习python语句和模块
linux·运维·开发语言·python·学习
橘子是码猴子1 天前
LangExtract:基于LLM的信息抽取框架 学习笔记
笔记·学习