InterView-OC基础-2.KVC

389 阅读9分钟

OC iOS Cocoa的kvc实现原理及所有用法

ChatGPT 说:

ChatGPT

在 Objective-C 的 iOS 开发中,KVC(Key-Value Coding,键值编码)是一种通过字符串访问对象属性的方式,是 Foundation 框架的一部分。KVC 的实现原理和用法如下:


KVC 实现原理

1. 基本原理

KVC 的核心依赖于以下方法:

  • - (id)valueForKey:(NSString *)key
  • - (void)setValue:(id)value forKey:(NSString *)key

当通过 valueForKey:setValue:forKey: 访问属性时,KVC 使用以下规则查找和操作属性:

1.1 Getter 查找顺序valueForKey:

对于 valueForKey:,KVC 会按照以下顺序查找 key 对应的值:

  1. 直接调用 getter 方法
    如果存在名为 get<Key><Key> 的方法,直接调用返回值。
  2. 查找实例变量
    如果没有找到对应的方法,会查找名为 _key_is<Key>keyis<Key> 的实例变量。
  3. 调用 valueForUndefinedKey:
    如果以上均未找到,调用 valueForUndefinedKey:,默认抛出异常。

1.2 Setter 查找顺序setValue:forKey:

对于 setValue:forKey:,KVC 会按以下顺序查找:

  1. 直接调用 setter 方法
    如果存在名为 set<Key>: 的方法,调用它。
  2. 查找实例变量
    如果没有 setter 方法,直接尝试访问名为 _key_is<Key>keyis<Key> 的实例变量。
  3. 调用 setValue:forUndefinedKey:
    如果以上均未找到,调用 setValue:forUndefinedKey:,默认抛出异常。

2. 常用方法

方法功能描述
valueForKey:通过 key 获取属性值。
setValue:forKey:通过 key 设置属性值。
valueForKeyPath:通过点语法路径(keyPath)获取嵌套属性值。
setValue:forKeyPath:通过点语法路径(keyPath)设置嵌套属性值。
dictionaryWithValuesForKeys:将多个 key 的值转成字典。
setValuesForKeysWithDictionary:通过字典批量设置属性值。

3. 依赖键机制(Key-Value Observing)

KVC 支持依赖键机制,结合 KVO 使用时非常强大。

  • 如果属性 fullName 依赖于 firstNamelastName,可以重写 + (NSSet *)keyPathsForValuesAffecting<Key> 指定依赖关系:

    objc
    复制代码
    + (NSSet *)keyPathsForValuesAffectingFullName {
        return [NSSet setWithObjects:@"firstName", @"lastName", nil];
    }
    

KVC 的用法

1. 基本用法

1.1 访问属性值

objc
复制代码
Person *person = [[Person alloc] init];
[person setValue:@"John" forKey:@"name"];
NSString *name = [person valueForKey:@"name"];

1.2 访问嵌套属性值

objc
复制代码
NSString *city = [person valueForKeyPath:@"address.city"];
[person setValue:@"New York" forKeyPath:@"address.city"];

2. 集合操作

KVC 提供强大的集合操作符,支持对数组和集合进行操作。

2.1 聚合操作符

  • @count: 计算集合元素数量
  • @sum: 求和
  • @avg: 计算平均值
  • @min: 找最小值
  • @max: 找最大值

示例:

objc
复制代码
NSArray *numbers = @[@1, @2, @3, @4];
NSNumber *count = [numbers valueForKeyPath:@"@count"];
NSNumber *sum = [numbers valueForKeyPath:@"@sum"];
NSNumber *avg = [numbers valueForKeyPath:@"@avg"];
NSNumber *min = [numbers valueForKeyPath:@"@min"];
NSNumber *max = [numbers valueForKeyPath:@"@max"];

2.2 数组操作

  • 获取属性数组:

    objc
    复制代码
    NSArray *names = [people valueForKey:@"name"];
    

3. 字典与批量操作

3.1 从对象生成字典

objc
复制代码
NSDictionary *dict = [person dictionaryWithValuesForKeys:@[@"name", @"age"]];

3.2 从字典批量设置值

objc
复制代码
[person setValuesForKeysWithDictionary:@{@"name": @"Jane", @"age": @25}];

4. 处理未定义的键

重写 valueForUndefinedKey:setValue:forUndefinedKey:,可以处理动态键或避免程序崩溃:

objc
复制代码
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"Undefined Key: %@", key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"Set undefined key: %@ with value: %@", key, value);
}

注意事项与最佳实践

  1. 安全性:KVC 通过字符串访问属性,容易导致运行时错误,需谨慎使用。
  2. 键路径拼写keykeyPath 的拼写必须正确,否则会抛出异常。
  3. 封装性:直接访问实例变量可能会破坏封装性,尽量使用 getter 和 setter 方法。
  4. 集合操作符:利用集合操作符简化代码,避免手动迭代。

通过掌握 KVC 的实现原理和用法,可以大幅提升代码的灵活性和简洁性,同时也为实现动态属性访问和响应式编程提供了基础。

4o

您说:

KVC的进阶的面试题

ChatGPT 说:

ChatGPT

在 iOS 开发的面试中,关于 KVC 的高级问题通常涉及其实现原理、边界情况、与其他机制的交互等。以下是一些常见的进阶 KVC 面试题及答案解析:


1. KVC 如何访问私有属性或变量?它是否会破坏封装性?

问题:
KVC 能否访问类的私有属性或实例变量?它如何做到这一点?是否有潜在风险?

回答:

  • 是的,KVC 能够访问私有属性和实例变量。
    在查找过程中,如果找不到 gettersetter 方法,KVC 会直接尝试访问实例变量,包括 privateprotected 的实例变量。

  • KVC 使用了 Objective-C 的运行时特性(如 objc_property_tivar)绕过了访问控制。

  • 风险:

    • 破坏了封装性,外部对象可以直接访问和修改私有变量。
    • 如果修改了类的内部状态而没有触发必要的逻辑(如通知或验证),可能会引发不可预期的行为。

示例代码:

objc
复制代码
@interface Person : NSObject {
@private
    NSString *_privateName;
}
@end

@implementation Person
@end

// 测试代码
Person *person = [[Person alloc] init];
[person setValue:@"John" forKey:@"_privateName"];
NSLog(@"%@", [person valueForKey:@"_privateName"]); // 输出: John

2. KVC 是否支持动态属性?如果支持,如何实现?

问题:
如何使用 KVC 实现动态属性访问?

回答:
KVC 可以通过重写 valueForUndefinedKey:setValue:forUndefinedKey: 方法支持动态属性访问。

示例代码:

objc
复制代码
@interface DynamicObject : NSObject
@property (nonatomic, strong) NSMutableDictionary *dynamicProperties;
@end

@implementation DynamicObject

- (instancetype)init {
    if (self = [super init]) {
        _dynamicProperties = [NSMutableDictionary dictionary];
    }
    return self;
}

- (id)valueForUndefinedKey:(NSString *)key {
    return self.dynamicProperties[key];
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    self.dynamicProperties[key] = value;
}

@end

// 测试代码
DynamicObject *obj = [[DynamicObject alloc] init];
[obj setValue:@"DynamicValue" forKey:@"customKey"];
NSLog(@"%@", [obj valueForKey:@"customKey"]); // 输出: DynamicValue

3. 如果给 nil 设置值,KVC 会发生什么?

问题:
当使用 setValue:forKey:nil 设置值时,程序会出现什么问题?

回答:

  • 如果 valuenil

    • 如果属性是非对象类型(如 intfloat),KVC 会尝试将 nil 转为默认值(如 0)。
    • 如果属性是对象类型,则会直接设置为 nil
  • 如果 keynil,KVC 会抛出异常。

示例代码:

objc
复制代码
// 对象类型
[person setValue:nil forKey:@"name"]; // name 被设置为 nil

// 非对象类型
[person setValue:nil forKey:@"age"]; // age 被设置为 0

4. 如何通过 KVC 修改只读属性?

问题:
KVC 是否可以修改 readonly 的属性?如何实现?

回答:

  • 虽然 readonly 属性没有提供 setter 方法,但 KVC 可以通过直接访问实例变量的方式绕过限制,从而修改 readonly 属性的值。
  • 注意:这种方式违反封装原则,应谨慎使用。

示例代码:

objc
复制代码
@interface Person : NSObject
@property (nonatomic, readonly) NSString *name;
@end

@implementation Person {
    NSString *_name; // KVC 会直接访问这个实例变量
}
@end

Person *person = [[Person alloc] init];
[person setValue:@"NewName" forKey:@"name"];
NSLog(@"%@", [person valueForKey:@"name"]); // 输出: NewName

5. 使用 KVC 修改集合属性时,有什么需要注意的?

问题:
KVC 如何操作集合属性?与直接修改集合有何区别?

回答:

  • KVC 提供了对集合属性的访问器方法,可以安全地修改集合,同时自动触发 KVO 通知。

  • 集合访问器方法:

    • mutableArrayValueForKey:
    • mutableSetValueForKey:
    • mutableOrderedSetValueForKey:

示例代码:

objc
复制代码
@interface Person : NSObject
@property (nonatomic, strong) NSMutableArray *friends;
@end

@implementation Person
@end

Person *person = [[Person alloc] init];
person.friends = [NSMutableArray arrayWithArray:@[@"Alice", @"Bob"]];

// 使用 KVC 的集合访问器
NSMutableArray *mutableFriends = [person mutableArrayValueForKey:@"friends"];
[mutableFriends addObject:@"Charlie"]; // 自动触发 KVO

**注意:**直接调用 friendsaddObject: 方法不会触发 KVO


6. 如果 valueForKey: 找不到对应的 key 会发生什么?

问题:
valueForKey: 找不到 key 时的行为是什么?如何处理这种情况?

回答:

  • 如果 KVC 在对象中找不到指定的 key,会调用 valueForUndefinedKey: 方法,默认实现会抛出异常。
  • 可以通过重写 valueForUndefinedKey: 方法来自定义行为,避免程序崩溃。

示例代码:

objc
复制代码
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"Undefined key: %@", key);
    return nil; // 或者返回一个默认值
}

7. valueForKey: 与直接调用 getter 的区别?

问题:
直接调用 getter 和通过 valueForKey: 获取值有何不同?

回答:

  1. 查找机制不同:

    • getter 直接调用对应的方法。
    • valueForKey: 会按照查找顺序(方法 -> 实例变量 -> valueForUndefinedKey:)进行查找。
  2. 灵活性:

    • valueForKey: 支持动态键名,可以用于动态属性访问。
    • getter 是编译期绑定,不支持动态键。

8. KVC 如何与 KVO 配合使用?

问题:
KVC 对 KVO 的支持如何体现?

回答:

  • KVO 的实现依赖于 KVC,当通过 KVC 修改属性值时,会自动触发 KVO 通知。
  • 如果直接通过实例变量修改值,则不会触发 KVO,除非显式调用 willChangeValueForKey:didChangeValueForKey:

示例代码:

objc
复制代码
[person setValue:@"NewName" forKey:@"name"]; // 自动触发 KVO

// 直接修改实例变量
[person willChangeValueForKey:@"name"];
_name = @"DirectName";
[person didChangeValueForKey:@"name"]; // 手动触发 KVO

通过理解这些问题,能够帮助候选人在面试中展示对 KVC 的深度理解,以及其在实际开发中的应用和潜在风险。

4o

您说:

如果给NSDictionary setvalue 为nil会有什么问题

ChatGPT 说:

ChatGPT

在 Objective-C 中,NSDictionary 是不可变的,无法直接通过 setValue:forKey: 方法设置值。通常我们会操作 NSMutableDictionary 来修改键值对。如果给 NSMutableDictionary 设置 nil 值,会引发以下问题:


行为分析

1. NSDictionary 不支持直接修改

NSDictionary 是不可变的,不能通过 setValue:forKey: 修改内容。如果尝试使用此方法,会直接报错(编译器会警告 No visible @interface for 'NSDictionary' declares the selector 'setValue:forKey:')。


2. NSMutableDictionary 的行为

情况 1:键值为 nil

NSMutableDictionary 不允许键或值为 nil。如果尝试插入 nil,会抛出运行时异常:

示例代码:

objc
复制代码
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setValue:nil forKey:@"key"]; // 崩溃,报错:Attempt to insert nil object

错误日志:

vbnet
复制代码
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: 'Attempt to insert nil object from objects[0]'

情况 2:移除键值对

  • 如果 valuenilNSMutableDictionary 会将其解释为移除对应键值对,而不是插入一个 nil 值。
  • setValue:nil forKey:key 的效果等同于调用 removeObjectForKey:

示例代码:

objc
复制代码
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[@"key"] = @"value";
[dict setValue:nil forKey:@"key"]; // 移除 "key" 对应的键值对
NSLog(@"%@", dict); // 输出:{}

3. 使用防止崩溃的方式

为避免向 NSMutableDictionary 插入 nil 引发崩溃,可以采取以下措施:

方法 1:判空检查

在设置值之前检查是否为 nil

objc
复制代码
id value = ...; // 可能为 nil
if (value) {
    [dict setValue:value forKey:@"key"];
}

方法 2:使用默认值

valuenil 时,提供一个默认值(如 NSNull):

objc
复制代码
[dict setValue:(value ?: [NSNull null]) forKey:@"key"];

方法 3:直接移除键

如果希望在 valuenil 时移除键值对,可以直接调用 removeObjectForKey:

objc
复制代码
if (!value) {
    [dict removeObjectForKey:@"key"];
} else {
    [dict setValue:value forKey:@"key"];
}

总结

  1. NSDictionary 设置值(包括 nil)会导致编译错误。

  2. NSMutableDictionary 设置值为 nil 会导致以下行为:

    • 如果值为 nil,将移除对应的键值对。
    • 如果键或值直接是 nil,会引发运行时异常。
  3. 推荐在插入 NSMutableDictionary 前进行判空检查或提供默认值,以防止崩溃。

4o

O

ChatGPT 也可能会犯错。请核查重要信息。