iOS开发基础14-KVC

KVC(Key-Value Coding)深度解析:属性访问、字典转换与集合运算符

KVC(Key-Value Coding)是一种通过字符串键在运行时访问和修改对象属性的机制。它为字典转模型、KVO、Core Data 等技术提供了底层支持,是 Objective-C 运行时的重要特性之一。


一、KVC 简介

KVC 是定义在 NSObject 上的非正式协议(NSKeyValueCoding 分类),允许通过字符串 Key 间接访问对象属性,而非直接调用 setter/getter。这种动态特性在字典转模型、集合操作、数据绑定等场景中极为有用。

Swift 中的 KVC:Swift 类使用 KVC 必须继承自 NSObject,且属性需标记 @objc。Swift 原生更推荐使用 Codable 和 KeyPath,但与 UIKit 交互时仍会用到 KVC。


二、KVC 存数据

1. 演示用模型

// Dog.h
@interface Dog : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) double price;
@end

// Person.h
@class Dog;
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) double money;
@property (nonatomic, strong) Dog *dog;
@end
// Person.m
@implementation Person {
    @private
    int _age; // 私有成员变量
}
- (void)say { NSLog(@"age = %i", _age); }
@end

2. 基本属性赋值

Objective-C

Person *p = [[Person alloc] init];

// 基本类型会自动装箱/拆箱,传入 NSNumber 即可
[p setValue:@"lmj" forKey:@"name"];
[p setValue:@(668.0) forKey:@"money"];

Swift

class Person: NSObject {
    @objc var name: String = ""
    @objc var money: Double = 0
    @objc var dog: Dog?
}

let p = Person()
p.setValue("lmj", forKey: "name")
p.setValue(668.0, forKey: "money")

3. 多层路径赋值(KeyPath)

对于嵌套对象的属性,使用 setValue:forKeyPath:,用点号分隔路径:

p.dog = [[Dog alloc] init];
[p setValue:@"xiaoqiang" forKeyPath:@"dog.name"];
[p setValue:@(110.0) forKeyPath:@"dog.price"];
p.dog = Dog()
p.setValue("xiaoqiang", forKeyPath: "dog.name")
p.setValue(110.0, forKeyPath: "dog.price")

KeyPath 中每一级对象都不能为 nil,否则赋值无效(不会崩溃,但目标属性不会被设置)。

4. 访问私有成员变量

KVC 可以突破访问修饰符限制,直接访问私有实例变量:

[p setValue:@(30) forKey:@"_age"]; // 直接设置私有变量 _age
[p say]; // 输出: age = 30

前提是 accessInstanceVariablesDirectly 返回 YES(默认即为 YES)。若重写该方法返回 NO,KVC 将不会直接访问实例变量。

5. 字典转模型

setValuesForKeysWithDictionary: 可将字典中的键值对批量赋值给对象属性:

NSDictionary *dict = @{
    @"name": @"xxx",
    @"money": @(998.1)
};
[p setValuesForKeysWithDictionary:dict];

重要注意事项

  1. 只做一层转换:setValuesForKeysWithDictionary: 不会自动将嵌套字典转为模型对象。如果字典中 @"dog" 对应一个 NSDictionary,它会被直接赋值给 dog 属性,导致类型错误。嵌套模型需手动处理或使用 MJExtension / YYModel 等库。

  2. 不存在的 key 会崩溃:如果字典中包含对象不存在的 key,默认会调用 setValue:forUndefinedKey: 并抛出 NSUnknownKeyException。可重写该方法安全处理:

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    // 忽略不存在的 key,或打印日志
    NSLog(@"⚠️ 未定义的 key: %@", key);
}

三、KVC 取数据

1. 获取单个值

NSString *name = [p valueForKey:@"name"];
double money = [[p valueForKey:@"money"] doubleValue]; // 基本类型返回 NSNumber,需拆箱

2. 多层路径获取

NSString *dogName = [p valueForKeyPath:@"dog.name"];

3. 模型转字典

dictionaryWithValuesForKeys: 将指定属性提取为字典:

NSDictionary *dict = [p dictionaryWithValuesForKeys:@[@"name", @"money"]];
// 输出: { name = xxx; money = 998.1; }

4. 数组上的 KVC

对数组调用 valueForKeyPath: 时,会对数组中每个元素执行操作,并返回结果数组:

Person *p1 = [[Person alloc] init]; p1.name = @"zs"; p1.money = 111;
Person *p2 = [[Person alloc] init]; p2.name = @"ls"; p2.money = 222;
Person *p3 = [[Person alloc] init]; p3.name = @"ww"; p3.money = 666;

NSArray *people = @[p1, p2, p3];
NSArray *names = [people valueForKeyPath:@"name"];
// 输出: (zs, ls, ww)

5. KVC 集合运算符

在 KeyPath 中使用 @运算符 可对集合进行聚合计算,返回 NSNumber:

运算符 作用
@avg 平均值
@sum 求和
@max 最大值
@min 最小值
@count 元素个数
@distinctUnionOfObjects 去重后的属性值数组
@unionOfObjects 不去重的属性值数组
// 平均值
NSNumber *avgMoney = [people valueForKeyPath:@"@avg.money"];

// 求和
NSNumber *sumMoney = [people valueForKeyPath:@"@sum.money"];

// 最大值 / 最小值
NSNumber *maxMoney = [people valueForKeyPath:@"@max.money"];
NSNumber *minMoney = [people valueForKeyPath:@"@min.money"];

// 去重
NSArray *uniqueNames = [people valueForKeyPath:@"@distinctUnionOfObjects.name"];

四、KVC 底层逻辑

1. valueForKey: 查找流程

以 valueForKey:@"name" 为例,系统按以下顺序查找:

  1. 查找 getter 方法:依次查找 getName、name、isName(针对 BOOL 类型)。
  2. 查找集合方法:查找 countOfName、objectInNameAtIndex:(有序集合)或 enumeratorOfName、memberOfName:(无序集合),用于代理集合属性。
  3. 查找实例变量:若 accessInstanceVariablesDirectly 返回 YES,依次查找 _name、_isName、name、isName。
  4. 都未找到:调用 valueForUndefinedKey:,默认抛出 NSUnknownKeyException。

2. setValue:forKey: 查找流程

以 setValue:forKey:@"name" 为例:

  1. 查找 setter 方法:查找 setName:。
  2. 查找实例变量:若 accessInstanceVariablesDirectly 返回 YES,依次查找 _name、_isName、name、isName。
  3. 都未找到:调用 setValue:forUndefinedKey:,默认抛出异常。

3. 与 KVO 的关系

  • 通过 KVC 的 setValue:forKey: 修改属性时,会自动触发 KVO 通知(前提是属性的 automaticallyNotifiesObserversForKey: 返回 YES,默认即为 YES)。
  • 直接访问实例变量(如 _name = xxx)不会触发 KVO,因为绕过了 setter 方法。
  • KVC 是 KVO 实现的基础之一,KVO 本质上是通过 swizzle setter 方法实现的。

4. 字典转模型的局限

能力 setValuesForKeysWithDictionary: MJExtension / YYModel
一层属性转换 支持 支持
嵌套模型自动转换 不支持 支持
数组中模型转换 不支持 支持
key 与属性名映射 不支持 支持(如 mj_replacedKeyFromPropertyName)
不存在 key 的处理 崩溃(需重写方法) 自动忽略

五、总结

  • KVC 通过字符串 Key 动态访问属性,是 Objective-C 运行时动态特性的体现。
  • setValue:forKey: / valueForKey: 操作单层属性,forKeyPath: 支持点语法的多层路径。
  • setValuesForKeysWithDictionary: 只做一层转换,嵌套模型需手动处理或使用第三方库。
  • 数组上的 KVC 支持 @avg、@sum、@max、@min、@distinctUnionOfObjects 等集合运算符。
  • KVC 可以访问私有成员变量,但依赖 accessInstanceVariablesDirectly 的返回值。
  • KVC 是 KVO、Core Data、字典转模型等技术的底层基础。
  • Swift 中使用 KVC 需继承 NSObject 并标记 @objc,新代码优先使用 Codable 和 KeyPath。

posted @ 2015-07-22 00:16  Mr.陳  阅读(332)  评论(0)    收藏  举报