1. KVC 是什么
1.1 基本概念
Key-Value Coding(KVC) 是 Cocoa 提供的一种间接访问对象属性的机制。通过字符串 key(键)来读写对象的属性,而不需要直接调用 setter/getter 方法或访问实例变量。
KVC 的核心协议是 NSKeyValueCoding,定义在 Foundation 框架中,所有 NSObject 子类都默认支持。
Swift 中使用 KVC 需要类继承自
NSObject并用@objc或dynamic标记属性。
1.2 直接访问 vs KVC 对比
| 方式 | 语法 | 类型安全 | 运行时灵活性 |
|---|---|---|---|
| 直接访问 | obj.name |
✅ 编译期检查 | ❌ 固定访问路径 |
| 点语法 / 方法调用 | [obj name] |
✅ 编译期检查 | ❌ 固定方法名 |
| KVC | [obj valueForKey:@"name"] |
❌ 运行时才知道 | ✅ 键名可动态传入 |
1.3 代码对比示例
直接访问:
ini
// ObjC - 直接访问
Person *p = [[Person alloc] init];
p.name = @"冯帆";
NSString *name = p.name;
ini
// Swift - 直接访问
let p = Person()
p.name = "冯帆"
let name = p.name
KVC 方式:
ini
// ObjC - KVC 方式
Person *p = [[Person alloc] init];
[p setValue:@"冯帆" forKey:@"name"];
NSString *name = [p valueForKey:@"name"];
less
// Swift - KVC 方式(需要继承 NSObject)
let p = Person()
p.setValue("冯帆", forKey: "name")
let name = p.value(forKey: "name") as? String
1.4 KVC 的用途
- 动态配置对象属性(批量赋值)
- 通过字符串键路径访问嵌套属性
- 对集合进行聚合运算(求和、求平均等)
- KVO(Key-Value Observing)的底层基础
- CoreData 属性访问
- JSON → Model 的映射
2. 核心方法
2.1 方法总览
| 方法 | 作用 |
|---|---|
valueForKey: |
根据键读取值 |
setValue:forKey: |
根据键设置值 |
valueForKeyPath: |
根据键路径读取值(支持嵌套) |
setValue:forKeyPath: |
根据键路径设置值 |
dictionaryWithValuesForKeys: |
批量读取多个键的值,返回字典 |
setValuesForKeysWithDictionary: |
用字典批量设置多个键的值 |
2.2 valueForKey: ------ 读取值
less
// ObjC
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
Person *person = [[Person alloc] init];
person.name = @"冯帆";
person.age = 28;
NSString *name = [person valueForKey:@"name"]; // @"冯帆"
NSNumber *age = [person valueForKey:@"age"]; // @28(自动装箱)
less
// Swift
class Person: NSObject {
@objc var name: String = ""
@objc var age: Int = 0
}
let person = Person()
person.name = "冯帆"
person.age = 28
let name = person.value(forKey: "name") as? String // "冯帆"
let age = person.value(forKey: "age") as? Int // 28
⚠️ 标量类型(int、float、BOOL 等)会被自动装箱为
NSNumber。
2.3 setValue:forKey: ------ 设置值
scss
// ObjC
Person *person = [[Person alloc] init];
[person setValue:@"冯帆" forKey:@"name"];
[person setValue:@28 forKey:@"age"]; // NSNumber 会自动拆箱
NSLog(@"%@, %ld", person.name, person.age); // 冯帆, 28
php
// Swift
let person = Person()
person.setValue("冯帆", forKey: "name")
person.setValue(28, forKey: "age")
print(person.name, person.age) // 冯帆 28
2.4 valueForKeyPath: ------ 键路径读取
键路径用点(.)连接,支持嵌套访问对象属性。
less
// ObjC
@interface Address : NSObject
@property (nonatomic, copy) NSString *city;
@end
@interface Person : NSObject
@property (nonatomic, strong) Address *address;
@end
Person *person = [[Person alloc] init];
person.address = [[Address alloc] init];
person.address.city = @"北京";
NSString *city = [person valueForKeyPath:@"address.city"]; // @"北京"
kotlin
// Swift
class Address: NSObject {
@objc var city: String = ""
}
class Person: NSObject {
@objc var address: Address = Address()
}
let person = Person()
person.address.city = "北京"
let city = person.value(forKeyPath: "address.city") as? String // "北京"
2.5 setValue:forKeyPath: ------ 键路径设置
ini
// ObjC
Person *person = [[Person alloc] init];
person.address = [[Address alloc] init];
[person setValue:@"上海" forKeyPath:@"address.city"];
NSLog(@"%@", person.address.city); // 上海
scss
// Swift
let person = Person()
person.address = Address()
person.setValue("上海", forKeyPath: "address.city")
print(person.address.city) // 上海
2.6 dictionaryWithValuesForKeys: ------ 批量读取
传入一组 key,返回包含这些 key-value 的字典。
ini
// ObjC
Person *person = [[Person alloc] init];
person.name = @"冯帆";
person.age = 28;
NSArray *keys = @[@"name", @"age"];
NSDictionary *dict = [person dictionaryWithValuesForKeys:keys];
// { "name": "冯帆", "age": 28 }
ini
// Swift
let person = Person()
person.name = "冯帆"
person.age = 28
let dict = person.dictionaryWithValues(forKeys: ["name", "age"])
// ["name": "冯帆", "age": 28]
如果某个 key 对应的值为
nil,字典中对应 value 会是NSNull.null。
2.7 setValuesForKeysWithDictionary: ------ 批量设置
用字典的 key-value 批量赋值给对象。
scss
// ObjC
NSDictionary *info = @{
@"name": @"冯帆",
@"age" : @28
};
Person *person = [[Person alloc] init];
[person setValuesForKeysWithDictionary:info];
NSLog(@"%@, %ld", person.name, person.age); // 冯帆, 28
swift
// Swift
let info: [String: Any] = [
"name": "冯帆",
"age" : 28
]
let person = Person()
person.setValuesForKeys(info)
print(person.name, person.age) // 冯帆 28
⚠️ 字典中出现对象没有的 key,会触发
setValue:forUndefinedKey:,默认抛异常,需要重写处理。
3. Key Path
3.1 嵌套访问
多级嵌套用 . 连接:
ini
// ObjC
// 访问 person.company.address.city
NSString *city = [person valueForKeyPath:@"company.address.city"];
javascript
// Swift
let city = person.value(forKeyPath: "company.address.city") as? String
3.2 集合运算符
对集合(NSArray、NSSet 等)使用 KVC 时,Key Path 支持一组特殊的集合运算符,格式为:
xml
@<operator>
@<operator>.<keyPath>
完整运算符列表
| 运算符 | 说明 | 返回类型 |
|---|---|---|
@count |
元素个数 | NSNumber |
@sum.keyPath |
求和 | NSNumber |
@avg.keyPath |
求平均 | NSNumber |
@max.keyPath |
最大值 | 对应属性类型 |
@min.keyPath |
最小值 | 对应属性类型 |
@distinctUnionOfObjects.keyPath |
去重后的值列表 | NSArray |
@unionOfObjects.keyPath |
不去重的值列表 | NSArray |
@unionOfArrays.keyPath |
嵌套数组展平合并 | NSArray |
@distinctUnionOfArrays.keyPath |
嵌套数组展平去重 | NSArray |
示例数据模型
less
// ObjC
@interface Product : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGFloat price;
@property (nonatomic, assign) NSInteger stock;
@end
ini
// ObjC - 创建测试数据
NSArray *products = @[
product(@"iPhone", 7999.0, 10),
product(@"iPad", 4999.0, 5),
product(@"Mac", 12999.0, 3),
product(@"iPhone", 7999.0, 8), // 重复
];
@count ------ 元素个数
objectivec
// ObjC
NSNumber *count = [products valueForKeyPath:@"@count"];
NSLog(@"%@", count); // 4
csharp
// Swift
let count = products.value(forKeyPath: "@count") as? Int // 4
@sum ------ 求和
objectivec
// ObjC
NSNumber *totalPrice = [products valueForKeyPath:@"@sum.price"];
NSLog(@"%@", totalPrice); // 33996
csharp
// Swift
let totalPrice = products.value(forKeyPath: "@sum.price") as? Double
@avg ------ 求平均
objectivec
// ObjC
NSNumber *avgPrice = [products valueForKeyPath:@"@avg.price"];
NSLog(@"%.2f", avgPrice.doubleValue); // 8499.00
csharp
// Swift
let avgPrice = products.value(forKeyPath: "@avg.price") as? Double
@max / @min ------ 最大/最小
ini
// ObjC
NSNumber *maxPrice = [products valueForKeyPath:@"@max.price"]; // 12999
NSNumber *minPrice = [products valueForKeyPath:@"@min.price"]; // 4999
vbnet
// Swift
let maxPrice = products.value(forKeyPath: "@max.price") as? Double
let minPrice = products.value(forKeyPath: "@min.price") as? Double
@distinctUnionOfObjects ------ 去重列表
css
// ObjC
NSArray *uniqueNames = [products valueForKeyPath:@"@distinctUnionOfObjects.name"];
// ["iPhone", "iPad", "Mac"](顺序不保证)
javascript
// Swift
let uniqueNames = products.value(forKeyPath: "@distinctUnionOfObjects.name") as? [String]
@unionOfObjects ------ 不去重列表
css
// ObjC
NSArray *allNames = [products valueForKeyPath:@"@unionOfObjects.name"];
// ["iPhone", "iPad", "Mac", "iPhone"]
@unionOfArrays / @distinctUnionOfArrays ------ 嵌套数组操作
ini
// ObjC
// 假设每个 store 有一个 products 数组
NSArray *stores = @[storeA, storeB]; // 每个 store 的 products 是 NSArray
// 展平所有 store 的商品列表
NSArray *allProducts = [stores valueForKeyPath:@"@unionOfArrays.products"];
// 展平并去重
NSArray *uniqueProducts = [stores valueForKeyPath:@"@distinctUnionOfArrays.products"];
4. KVC 搜索顺序
4.1 valueForKey: 的搜索顺序
当调用 [obj valueForKey:@"name"] 时,运行时按以下顺序查找:
markdown
1. 查找 getter 方法
- getName
- name
- isName
- _name(以下划线开头)
2. 如果 accessInstanceVariablesDirectly == YES(默认)
按顺序查找实例变量:
- _name
- _isName
- name
- isName
3. 以上都找不到
- 调用 valueForUndefinedKey:
- 默认抛出 NSUndefinedKeyException
流程图示:
objectivec
valueForKey:@"name"
│
▼
找 getter 方法?
getName / name / isName / _name
│
找到 ──────────────────► 返回值
│
未找到
│
▼
accessInstanceVariablesDirectly == YES?
│
NO ────────────────────► valueForUndefinedKey: → 抛异常
│
YES
│
▼
找实例变量?
_name / _isName / name / isName
│
找到 ──────────────────► 返回值
│
未找到
│
▼
valueForUndefinedKey: → 抛异常
4.2 setValue:forKey: 的搜索顺序
markdown
1. 查找 setter 方法
- setName:
- _setName:(私有 setter)
2. 如果 accessInstanceVariablesDirectly == YES
按顺序查找实例变量直接赋值:
- _name
- _isName
- name
- isName
3. 都找不到
- 调用 setValue:forUndefinedKey:
- 默认抛出 NSUndefinedKeyException
4.3 accessInstanceVariablesDirectly
这是 NSObject 的一个类方法,控制 KVC 是否允许直接访问实例变量(绕过 setter/getter)。
objectivec
// ObjC - 禁止直接访问实例变量
@implementation SecureModel
+ (BOOL)accessInstanceVariablesDirectly {
return NO; // 默认是 YES
}
@end
kotlin
// Swift
class SecureModel: NSObject {
override class func accessInstanceVariablesDirectly() -> Bool {
return false // 禁止实例变量直接访问
}
}
| 值 | 效果 |
|---|---|
YES(默认) |
找不到方法时,尝试直接读写实例变量 |
NO |
找不到方法时,直接触发 valueForUndefinedKey: |
4.4 找不到 Key 时的处理
默认行为是抛出 NSUndefinedKeyException 异常,程序崩溃。
可以通过重写以下方法优雅处理:
objectivec
// ObjC
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"⚠️ valueForUndefinedKey: %@", key);
return nil; // 返回 nil 而不是崩溃
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"⚠️ setValue:forUndefinedKey: %@", key);
// 忽略不认识的 key
}
swift
// Swift
override func value(forUndefinedKey key: String) -> Any? {
print("⚠️ value(forUndefinedKey:) (key)")
return nil
}
override func setValue(_ value: Any?, forUndefinedKey key: String) {
print("⚠️ setValue(_:forUndefinedKey:) (key)")
}
5. 集合操作
5.1 对数组元素批量取值
对 NSArray 调用 valueForKey:,会对每个元素执行该操作,返回结果数组。
ini
// ObjC
NSArray *people = @[person1, person2, person3];
NSArray *names = [people valueForKey:@"name"];
// 等价于:[person1.name, person2.name, person3.name]
NSLog(@"%@", names); // ("冯帆", "李华", "王明")
javascript
// Swift
let people: [Person] = [person1, person2, person3]
let names = people.value(forKey: "name") as? [String]
如果某个元素该属性为
nil,对应位置返回NSNull。
5.2 NSSet 和 NSOrderedSet 的 KVC
NSSet 和 NSOrderedSet 同样支持 KVC 集合运算,行为与 NSArray 类似。
ini
// ObjC - NSSet
NSSet *personSet = [NSSet setWithObjects:person1, person2, nil];
NSSet *nameSet = [personSet valueForKey:@"name"];
NSNumber *avgAge = [personSet valueForKeyPath:@"@avg.age"];
javascript
// Swift - NSSet
let personSet: NSSet = [person1, person2]
let nameSet = personSet.value(forKey: "name") as? Set<String>
let avgAge = personSet.value(forKeyPath: "@avg.age") as? Double
5.3 可变集合代理 ------ mutableArrayValueForKey:
mutableArrayValueForKey: 返回一个可变数组代理对象,对该代理的增删操作会自动映射到 KVO 通知,是实现集合 KVO 的关键。
less
// ObjC
@interface Team : NSObject
@property (nonatomic, strong) NSMutableArray *members;
@end
Team *team = [[Team alloc] init];
team.members = [NSMutableArray array];
// 获取可变数组代理(会触发 KVO)
NSMutableArray *proxy = [team mutableArrayValueForKey:@"members"];
[proxy addObject:@"冯帆"];
[proxy removeObjectAtIndex:0];
csharp
// Swift
class Team: NSObject {
@objc dynamic var members: [String] = []
}
let team = Team()
let proxy = team.mutableArrayValue(forKey: "members")
proxy.add("冯帆")
5.4 可变集合代理 ------ mutableSetValueForKey:
less
// ObjC
@interface Group : NSObject
@property (nonatomic, strong) NSMutableSet *tags;
@end
Group *group = [[Group alloc] init];
group.tags = [NSMutableSet set];
NSMutableSet *proxy = [group mutableSetValueForKey:@"tags"];
[proxy addObject:@"iOS"];
[proxy addObject:@"Swift"];
csharp
// Swift
class Group: NSObject {
@objc dynamic var tags: NSMutableSet = NSMutableSet()
}
let group = Group()
let proxy = group.mutableSetValue(forKey: "tags")
proxy.add("iOS")
5.5 可变有序集合代理 ------ mutableOrderedSetValueForKey:
less
// ObjC
@interface Playlist : NSObject
@property (nonatomic, strong) NSMutableOrderedSet *songs;
@end
Playlist *pl = [[Playlist alloc] init];
pl.songs = [NSMutableOrderedSet orderedSet];
NSMutableOrderedSet *proxy = [pl mutableOrderedSetValueForKey:@"songs"];
[proxy addObject:@"Song A"];
[proxy insertObject:@"Song B" atIndex:0];
swift
// Swift
class Playlist: NSObject {
@objc dynamic var songs: NSMutableOrderedSet = NSMutableOrderedSet()
}
let pl = Playlist()
let proxy = pl.mutableOrderedSetValue(forKey: "songs")
proxy.add("Song A")
5.6 支持可变集合代理的方法命名规范
为了让代理对象正常工作,需要实现对应的集合访问方法(可选但推荐):
Array(有序集合)
| 方法 | 说明 |
|---|---|
countOf<Key> |
返回元素个数 |
objectIn<Key>AtIndex: |
按索引取元素 |
<key>AtIndexes: |
按 NSIndexSet 取元素 |
insertObject:in<Key>AtIndex: |
插入元素(可变) |
removeObjectFrom<Key>AtIndex: |
删除元素(可变) |
replaceObjectIn<Key>AtIndex:withObject: |
替换元素(可变) |
Set(无序集合)
| 方法 | 说明 |
|---|---|
countOf<Key> |
元素个数 |
enumeratorOf<Key> |
返回枚举器 |
memberOf<Key>: |
判断是否包含 |
add<Key>Object: |
添加(可变) |
remove<Key>Object: |
删除(可变) |
6. 验证机制
6.1 validateValue:forKey:error:
KVC 提供了内置的值验证机制,在设值之前可以验证数据的合法性。
objectivec
// ObjC - 定义验证方法(在 Model 中)
@implementation Person
// 验证 age 字段
- (BOOL)validateAge:(id *)ioValue error:(NSError **)outError {
NSNumber *age = *ioValue;
if (age == nil) {
// 允许 nil 则返回 YES,否则设置 error 后返回 NO
if (outError) {
*outError = [NSError errorWithDomain:@"PersonError"
code:1
userInfo:@{NSLocalizedDescriptionKey: @"年龄不能为空"}];
}
return NO;
}
if (age.integerValue < 0 || age.integerValue > 150) {
if (outError) {
*outError = [NSError errorWithDomain:@"PersonError"
code:2
userInfo:@{NSLocalizedDescriptionKey: @"年龄必须在 0-150 之间"}];
}
return NO;
}
return YES;
}
@end
ini
// ObjC - 调用验证
Person *person = [[Person alloc] init];
NSNumber *age = @(-5);
NSError *error = nil;
BOOL valid = [person validateValue:&age forKey:@"age" error:&error];
if (valid) {
[person setValue:age forKey:@"age"];
} else {
NSLog(@"验证失败: %@", error.localizedDescription);
// 输出:验证失败: 年龄必须在 0-150 之间
}
swift
// Swift
class Person: NSObject {
@objc var age: Int = 0
@objc func validateAge(_ ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>,
error outError: NSErrorPointer) -> Bool {
guard let value = ioValue.pointee as? Int else {
outError?.pointee = NSError(domain: "PersonError",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "年龄不能为空"])
return false
}
if value < 0 || value > 150 {
outError?.pointee = NSError(domain: "PersonError",
code: 2,
userInfo: [NSLocalizedDescriptionKey: "年龄必须在 0-150 之间"])
return false
}
return true
}
}
// 调用
let person = Person()
var ageValue: AnyObject? = -5 as AnyObject
var error: NSError?
let valid = person.validateValue(&ageValue, forKey: "age", error: &error)
if !valid {
print("验证失败: (error?.localizedDescription ?? "")")
}
⚠️
validateValue:forKey:error:不会自动被setValue:forKey:调用 ,需要手动调用。CoreData 的
NSManagedObject是个例外------它在save:时会自动触发验证。
7. nil 值处理
7.1 问题场景
当对一个标量类型 (如 int、float、BOOL、CGFloat)的属性设置 nil 时,KVC 无法自动处理,会调用 setNilValueForKey:,默认抛出异常:
swift
NSInvalidArgumentException: [<Person 0x...> setNilValueForKey]: could not set nil as the value for the key age.
7.2 重写 setNilValueForKey:
objectivec
// ObjC
@implementation Person
- (void)setNilValueForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
self.age = 0;
} else if ([key isEqualToString:@"score"]) {
self.score = 0.0;
} else {
[super setNilValueForKey:key]; // 其他 key 保持默认行为(抛异常)
}
}
@end
csharp
// ObjC - 触发场景
Person *person = [[Person alloc] init];
[person setValue:nil forKey:@"age"]; // 不重写会崩溃,重写后 age = 0
swift
// Swift - 重写
override func setNilValueForKey(_ key: String) {
switch key {
case "age": age = 0
case "score": score = 0.0
default: super.setNilValueForKey(key)
}
}
8. Undefined Key 处理
8.1 默认行为
访问不存在的 key 时,KVC 默认抛出:
vbnet
NSUndefinedKeyException: [<Person 0x...> valueForUndefinedKey:]: this class is not key value coding-compliant for the key xxx.
8.2 重写 valueForUndefinedKey: 和 setValue:forUndefinedKey:
objectivec
// ObjC - 读取未定义 key
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"⚠️ 读取了未定义的 key: %@", key);
return nil; // 返回 nil 而不是崩溃
}
// ObjC - 设置未定义 key
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"⚠️ 设置了未定义的 key: %@ = %@", key, value);
// 静默忽略,不崩溃
}
swift
// Swift
override func value(forUndefinedKey key: String) -> Any? {
print("⚠️ 读取未定义 key: (key)")
return nil
}
override func setValue(_ value: Any?, forUndefinedKey key: String) {
print("⚠️ 设置未定义 key: (key) = (String(describing: value))")
}
8.3 实际应用:安全的字典批量赋值
less
// ObjC - 安全地从字典赋值,忽略多余字段
@implementation SafeModel
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
// 静默忽略 JSON 中不认识的字段,避免崩溃
}
@end
NSDictionary *json = @{@"name": @"张三", @"age": @25, @"unknownField": @"xyz"};
SafeModel *model = [[SafeModel alloc] init];
[model setValuesForKeysWithDictionary:json]; // 不会因 unknownField 崩溃
9. KVC 与字典互转
9.1 字典 → 对象(批量赋值)
objectivec
// ObjC
@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *email;
@end
NSDictionary *dict = @{@"name": @"李四", @"age": @30, @"email": @"lisi@example.com"};
User *user = [[User alloc] init];
[user setValuesForKeysWithDictionary:dict];
// user.name == @"李四", user.age == 30
javascript
// Swift
let dict: [String: Any] = ["name": "李四", "age": 30, "email": "lisi@example.com"]
let user = User()
user.setValuesForKeys(dict)
9.2 对象 → 字典
sql
// ObjC
NSArray *keys = @[@"name", @"age", @"email"];
NSDictionary *dict = [user dictionaryWithValuesForKeys:keys];
// @{@"name": @"李四", @"age": @30, @"email": @"lisi@example.com"}
ini
// Swift
let keys = ["name", "age", "email"]
let dict = user.dictionaryWithValues(forKeys: keys)
9.3 注意事项
| 场景 | 问题 | 解决方案 |
|---|---|---|
| JSON key 与属性名不一致 | 赋值失败 | 重写 setValue:forUndefinedKey: 做映射 |
| JSON 有多余字段 | 崩溃 | 重写 setValue:forUndefinedKey: 静默忽略 |
| 属性是标量,JSON 值为 null | 崩溃 | 重写 setNilValueForKey: 设默认值 |
| 嵌套对象 | 不会自动递归创建 | 手动处理嵌套 |
10. KVC 的坑与注意事项
10.1 ⚠️ 类型安全问题
KVC 在编译期不做类型检查,所有错误在运行时爆发:
csharp
// ObjC - 编译通过,运行时崩溃
[person setValue:@"不是数字" forKey:@"age"];
// NSInvalidArgumentException: Cannot coerce value '不是数字' to int
10.2 ⚠️ 常见崩溃场景
csharp
// 1. key 拼写错误
[person setValue:@25 forKey:@"Age"]; // 大写A,找不到 → NSUndefinedKeyException
// 2. 标量属性赋 nil
[person setValue:nil forKey:@"age"]; // → 调 setNilValueForKey:,默认崩溃
// 3. 类型不兼容
[person setValue:@"abc" forKey:@"age"]; // → NSInvalidArgumentException
// 4. keyPath 路径不存在中间节点
// person.address 为 nil 时
[person valueForKeyPath:@"address.city"]; // 返回 nil(不崩溃,KVC 对此有保护)
10.3 ⚠️ 性能影响
KVC 通过字符串查找方法,比直接属性访问慢约 10~20 倍。
| 场景 | 建议 |
|---|---|
| 高频循环(万次以上) | 直接访问,不用 KVC |
| 配置型代码(一次性) | KVC 完全没问题 |
| CollectionOperator | 小集合 OK,大集合注意 |
csharp
// 性能对比示意
// 直接访问:~1ns
person.age = 25;
// KVC:~15ns(慢 15 倍)
[person setValue:@25 forKey:@"age"];
10.4 ⚠️ Swift 中的额外限制
- 结构体(struct)不支持 KVC(没有继承 NSObject)
- Swift 纯类需要继承
NSObject才能用 KVC - Swift 属性需要加
@objc才能被 KVC 访问(隐式桥接不总生效)
kotlin
// ❌ 不能用 KVC
struct Point {
var x: Double
}
// ✅ 可以用 KVC
class PointObj: NSObject {
@objc var x: Double = 0
}
10.5 accessInstanceVariablesDirectly
objectivec
// 类级别控制:禁止 KVC 访问实例变量(只允许通过方法)
+ (BOOL)accessInstanceVariablesDirectly {
return NO; // 默认是 YES
}
11. 实际应用场景
11.1 UI 组件批量配置
objectivec
// ObjC - 批量设置 UILabel 样式
NSDictionary *style = @{
@"textColor": [UIColor redColor],
@"font": [UIFont boldSystemFontOfSize:16],
@"textAlignment": @(NSTextAlignmentCenter)
};
for (UILabel *label in self.labels) {
[label setValuesForKeysWithDictionary:style];
}
11.2 轻量级 JSON 解析
less
// ObjC - 简单模型不引入第三方库时
@interface Article : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *content;
@property (nonatomic, assign) NSInteger viewCount;
@end
@implementation Article
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {} // 安全降级
- (void)setNilValueForKey:(NSString *)key { [self setValue:@0 forKey:key]; }
@end
NSDictionary *json = /* 来自网络的字典 */;
Article *article = [[Article alloc] init];
[article setValuesForKeysWithDictionary:json];
11.3 CoreData
CoreData 的 NSManagedObject 大量依赖 KVC:
ini
// ObjC - CoreData 对象
NSManagedObject *entity = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
inManagedObjectContext:context];
[entity setValue:@"王五" forKey:@"name"]; // CoreData 的标准写法
[entity setValue:@28 forKey:@"age"];
NSString *name = [entity valueForKey:@"name"];
11.4 单元测试访问私有属性
less
// ObjC - 测试中绕过封装访问私有状态
// 不推荐在生产代码里用,测试中可以
@interface MyViewModel : NSObject
// 私有属性 _internalState 没有暴露
@end
// 测试文件中
MyViewModel *vm = [[MyViewModel alloc] init];
// 强制访问私有变量
[vm setValue:@"mockState" forKey:@"internalState"];
XCTAssertEqualObjects([vm valueForKey:@"internalState"], @"mockState");
11.5 集合运算符实战
ini
// 统计购物车总价
NSArray *cartItems = @[item1, item2, item3]; // 每个 item 有 price 属性
NSNumber *total = [cartItems valueForKeyPath:@"@sum.price"];
// 去重品牌列表
NSArray *brands = [cartItems valueForKeyPath:@"@distinctUnionOfObjects.brand"];
// 最贵的商品价格
NSNumber *maxPrice = [cartItems valueForKeyPath:@"@max.price"];