MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

解析Objective-C中KVC(键值编码)的语法与原理

2023-09-146.8k 阅读

一、KVC 基础概念

在 Objective-C 编程中,键值编码(Key - Value Coding,简称 KVC)是一种通过键名间接访问对象属性的机制。这种机制提供了一种灵活且强大的方式来访问和修改对象的属性,而不依赖于传统的访问器方法(getter 和 setter)。

KVC 基于这样一个理念:对象的属性可以通过一个字符串键来标识。当你使用 KVC 来访问或修改对象的属性时,你提供一个表示属性名的键,KVC 机制会在对象内部查找对应的属性并执行相应的操作。

例如,假设有一个 Person 类,具有 nameage 属性:

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person
@end

通常情况下,我们通过属性访问器来访问和设置这些属性:

Person *person = [[Person alloc] init];
person.name = @"John";
person.age = 30;

使用 KVC 则可以这样做:

Person *person = [[Person alloc] init];
[person setValue:@"John" forKey:@"name"];
[person setValue:@30 forKey:@"age"];

这里,@"name"@"age" 就是键,通过它们可以间接设置 Person 对象的相应属性。

二、KVC 语法

  1. 基本的设置值语法 使用 setValue:forKey: 方法来设置对象属性的值。这个方法接受两个参数,第一个参数是要设置的值,第二个参数是表示属性名的键。
// 假设我们有一个 Car 类,具有 brand 和 price 属性
@interface Car : NSObject
@property (nonatomic, copy) NSString *brand;
@property (nonatomic, assign) CGFloat price;
@end

@implementation Car
@end

Car *car = [[Car alloc] init];
[car setValue:@"BMW" forKey:@"brand"];
[car setValue:@50000.0 forKey:@"price"];

需要注意的是,对于基本数据类型,如 intfloat 等,需要使用 NSNumber 进行包装,因为 KVC 方法接受的是对象类型。

  1. 基本的获取值语法 使用 valueForKey: 方法来获取对象属性的值。这个方法接受一个表示属性名的键作为参数,并返回对应属性的值。
Car *car = [[Car alloc] init];
[car setValue:@"BMW" forKey:@"brand"];
[car setValue:@50000.0 forKey:@"price"];

NSString *brand = [car valueForKey:@"brand"];
NSNumber *priceNumber = [car valueForKey:@"price"];
CGFloat price = [priceNumber floatValue];

同样,对于基本数据类型属性,返回的是包装后的 NSNumber 对象,需要进行相应的类型转换。

  1. 使用集合操作符的语法 KVC 提供了强大的集合操作符,用于对集合对象(如 NSArray、NSSet)中的元素进行操作。例如,计算数组中所有元素某个属性的总和、平均值等。 假设我们有一个 Employee 类,具有 salary 属性:
@interface Employee : NSObject
@property (nonatomic, assign) CGFloat salary;
@end

@implementation Employee
@end

然后创建一个 Employee 对象的数组:

Employee *emp1 = [[Employee alloc] init];
emp1.salary = 5000.0;
Employee *emp2 = [[Employee alloc] init];
emp2.salary = 6000.0;
Employee *emp3 = [[Employee alloc] init];
emp3.salary = 7000.0;
NSArray *employees = @[emp1, emp2, emp3];

要计算所有员工工资的总和,可以使用 @sum 集合操作符:

NSNumber *totalSalary = [employees valueForKeyPath:@"@sum.salary"];
CGFloat total = [totalSalary floatValue];

这里使用了 valueForKeyPath: 方法,并且在键路径中使用了集合操作符 @sum。常见的集合操作符还有 @avg(计算平均值)、@min(获取最小值)、@max(获取最大值)等。

三、KVC 原理

  1. 查找路径 当使用 setValue:forKey: 方法设置值时,KVC 遵循一套特定的查找路径。首先,它会检查对象是否有对应的 setter 方法,例如对于键 name,会查找 setName: 方法。如果找到了,就调用这个 setter 方法来设置值。 如果没有找到 setter 方法,KVC 会检查对象是否有一个名为 _<key> 的实例变量(例如 _name)。如果有,它会直接设置这个实例变量的值。 如果仍然没有找到,KVC 会检查对象是否有一个名为 <key> 的实例变量(例如 name)。如果有,也会直接设置这个实例变量的值。 最后,如果以上都没有找到,KVC 会调用对象的 setValue:forUndefinedKey: 方法,默认情况下这个方法会抛出一个异常。 例如,对于前面的 Person 类:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Person
- (void)setName:(NSString *)name {
    _name = name;
}
@end

当执行 [person setValue:@"John" forKey:@"name"] 时,KVC 首先会找到 setName: 方法并调用它来设置 name 属性的值。

  1. 获取值的原理 使用 valueForKey: 方法获取值时,KVC 也有类似的查找路径。首先,它会检查对象是否有对应的 getter 方法,例如对于键 name,会查找 nameisName(对于布尔类型属性)方法。如果找到了,就调用这个 getter 方法来获取值。 如果没有找到 getter 方法,KVC 会检查对象是否有一个名为 _<key> 的实例变量(例如 _name)。如果有,它会返回这个实例变量的值。 如果仍然没有找到,KVC 会检查对象是否有一个名为 <key> 的实例变量(例如 name)。如果有,也会返回这个实例变量的值。 最后,如果以上都没有找到,KVC 会调用对象的 valueForUndefinedKey: 方法,默认情况下这个方法会抛出一个异常。
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Person
- (NSString *)name {
    return _name;
}
@end

当执行 [person valueForKey:@"name"] 时,KVC 首先会找到 name 方法并调用它来获取 name 属性的值。

  1. 集合操作符原理 集合操作符的原理基于对集合中每个元素执行相同的 KVC 操作。以 @sum 操作符为例,KVC 会遍历集合中的每个元素,通过键路径获取每个元素对应的属性值,然后将这些值累加起来得到总和。 对于前面计算员工工资总和的例子:
NSNumber *totalSalary = [employees valueForKeyPath:@"@sum.salary"];

KVC 会遍历 employees 数组中的每个 Employee 对象,通过 @sum.salary 键路径,首先找到每个 Employee 对象的 salary 属性值,然后将这些值累加起来,最终返回总和。

四、KVC 与复杂对象结构

  1. 嵌套对象的访问 在实际应用中,对象结构可能非常复杂,包含嵌套的对象。KVC 提供了强大的键路径(Key Path)功能来访问嵌套对象的属性。 假设有一个 Department 类,包含一个 Manager 对象,而 Manager 类有一个 name 属性:
@interface Manager : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Manager
@end

@interface Department : NSObject
@property (nonatomic, strong) Manager *manager;
@end

@implementation Department
@end

可以通过键路径来访问 Department 对象中 Managername 属性:

Department *department = [[Department alloc] init];
Manager *manager = [[Manager alloc] init];
manager.name = @"Alice";
department.manager = manager;

NSString *managerName = [department valueForKeyPath:@"manager.name"];

这里的 manager.name 就是键路径,通过它可以跨越对象层次结构来访问属性。

  1. 集合嵌套的操作 当集合中包含嵌套对象时,KVC 同样可以发挥作用。例如,假设有一个 Company 类,包含一个 NSArray 类型的 departments 属性,每个 Department 对象又包含 employees 数组:
@interface Employee : NSObject
@property (nonatomic, assign) CGFloat salary;
@end

@implementation Employee
@end

@interface Department : NSObject
@property (nonatomic, strong) NSArray *employees;
@end

@implementation Department
@end

@interface Company : NSObject
@property (nonatomic, strong) NSArray *departments;
@end

@implementation Company
@end

要计算公司所有员工的工资总和,可以使用以下键路径:

Company *company = [[Company alloc] init];
// 初始化 departments 和 employees 等

NSNumber *totalSalary = [company valueForKeyPath:@"departments.@sum.employees.@sum.salary"];

这个复杂的键路径首先通过 departments 访问公司的各个部门,然后对每个部门中的 employees 数组使用 @sum 操作符计算工资总和,最后将所有部门的工资总和相加得到公司所有员工的工资总和。

五、KVC 的应用场景

  1. 数据绑定 在一些视图框架中,KVC 常用于数据绑定。例如,在 Cocoa 框架中,视图和数据模型之间可以通过 KVC 进行绑定。假设我们有一个文本框(UITextField)和一个数据模型对象(例如 User 类,具有 username 属性)。
@interface User : NSObject
@property (nonatomic, copy) NSString *username;
@end

@implementation User
@end

我们可以将文本框的 text 属性与 User 对象的 username 属性通过 KVC 绑定起来,这样当文本框中的文本发生变化时,User 对象的 username 属性也会相应更新,反之亦然。

User *user = [[User alloc] init];
UITextField *textField = [[UITextField alloc] init];
[textField bind:@"text" toObject:user withKeyPath:@"username" options:nil];
  1. 动态配置 在一些需要动态配置的场景中,KVC 非常有用。例如,一个应用程序可能需要根据用户的配置文件来动态设置某些对象的属性。假设配置文件中存储了一些键值对,如 {"windowColor": "red"}
@interface Window : NSObject
@property (nonatomic, strong) UIColor *windowColor;
@end

@implementation Window
@end

Window *window = [[Window alloc] init];
NSDictionary *config = @{@"windowColor": @"red"};
for (NSString *key in config) {
    id value = config[key];
    [window setValue:value forKey:key];
}

这里通过遍历配置字典,使用 KVC 动态设置 Window 对象的属性。

  1. 数据处理与聚合 在处理大量数据时,KVC 的集合操作符可以方便地进行数据聚合。例如,在一个电商应用中,有一个订单数组,每个订单包含商品数组,每个商品有价格属性。我们可以使用 KVC 快速计算所有订单中商品的总价格。
@interface Product : NSObject
@property (nonatomic, assign) CGFloat price;
@end

@implementation Product
@end

@interface Order : NSObject
@property (nonatomic, strong) NSArray *products;
@end

@implementation Order
@end

NSArray *orders = // 初始化订单数组
NSNumber *totalPrice = [orders valueForKeyPath:@"@sum.products.@sum.price"];

通过这种方式,可以简洁地实现复杂的数据聚合操作。

六、KVC 的注意事项

  1. 类型兼容性 在使用 KVC 设置值时,要注意类型兼容性。例如,不能将一个 NSString 对象设置到一个期望为 NSNumber 的属性上,除非进行适当的类型转换。
@interface SomeClass : NSObject
@property (nonatomic, assign) NSInteger number;
@end

@implementation SomeClass
@end

SomeClass *obj = [[SomeClass alloc] init];
// 以下代码会导致错误
[obj setValue:@"10" forKey:@"number"]; 

正确的做法是先将字符串转换为 NSNumber

NSNumber *number = @([@"10" integerValue]);
[obj setValue:number forKey:@"number"];
  1. 异常处理 当 KVC 找不到对应的键或执行操作失败时,会抛出异常。在实际应用中,需要适当处理这些异常,以避免程序崩溃。可以重写 setValue:forUndefinedKey:valueForUndefinedKey: 方法来提供自定义的错误处理逻辑。
@interface SomeClass : NSObject
@end

@implementation SomeClass
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"Undefined key %@", key);
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"Undefined key %@", key);
    return nil;
}
@end
  1. 性能考虑 虽然 KVC 提供了强大的功能,但由于其动态查找机制,性能可能不如直接调用访问器方法。在性能敏感的代码中,应尽量避免频繁使用 KVC。例如,在一个循环中对大量对象进行属性访问或设置时,使用直接访问器方法会更高效。
// 假设有大量的 Person 对象
NSArray *people = // 初始化人员数组
// 使用 KVC
for (Person *person in people) {
    [person setValue:@"New Name" forKey:@"name"];
}
// 使用直接访问器方法
for (Person *person in people) {
    person.name = @"New Name";
}

通常情况下,直接访问器方法的性能会优于 KVC。

通过深入理解 KVC 的语法和原理,以及在不同场景下的应用和注意事项,开发者可以在 Objective - C 编程中更灵活、高效地使用这一强大的机制,提升代码的质量和功能。无论是在简单的数据操作还是复杂的对象结构处理中,KVC 都能发挥重要的作用。