KVC
我不知道有多少人像本人一样,n 年来查看过不知道多少次这个 KVC 文档,但是每次通篇读下来总是会遇到读不懂的地方,总是莫名其妙的出现一个章节完全不知道在讲什么东西,不论是用传统的 Google 翻译或者目前主流的 AI 翻译翻译了,也还是会遇到读起来狗屁不通的情况。
- 其实根本原因是原始文档就有问题,东拼西凑的一坨屎,有歧义,导致不论你是直译还是意译都是让人摸不着头脑的翻译,只能根据上下文的理解才能正确翻译。为此作者将这份文档结合AI和个人理解做了全篇翻译,有需要的可以自行下载。
- 还有原始文档很多地方只写了要怎么做,但是没有告诉你为什么要这么做,就会让人觉得莫名其妙。
- 文档的有些内容太古老了,某些内容只在早期的 MacOS 开发中使用,在 iOS 开发中几乎接触,使用不到,但是也没有做任何说明。
这次作者决定结合 AI 和个人的理解将 KVC 文档彻底弄清楚,即使不能完全弄明白也要留下一个深刻的印象,方便后续遇到文章中的场景时能够回想起来。
章节概览
✅ 建议阅读
❌ 不建议阅读
入门指南
- ✅ 关于键值编码
键值编码基础知识
- ✅ 访问对象的属性
- ✅ 访问集合属性
- ✅ 使用集合操作符
- ✅ 表示非对象值
- ❌ 验证属性
- ✅ 访问器搜索逻辑
采用键值编码协议
- ❌ 实现基本键值编码合规性
- ❌ 定义集合方法
- ❌ 处理非对象值
- ❌ 添加验证
- ❌ 描述属性关系
- ❌ 以性能为导向进行设计
- ❌ 合规性检查清单
常见疑惑及解答
以下是作者对这篇文档的一些疑问以及结合 AI 的回答。
属性验证是什么鬼?
Person 有一个 NSString *name
属性,如果需要给 name 添加验证则在内部添加以下代码
1 | - (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{ |
在外部使用属性验证:
1 | Person* person = [[Person alloc] init]; |
谁会用到这么奇葩的功能?直接在 setter 方法里面对入参进行验证就非常合理了啊。为什么还要这么脱了裤子放屁?
上面的例子就是 KVC 的属性验证机制(validate
为什么会有 validate:error: 这种“奇葩”设计?
要理解它,就要从 KVC/KVO 的历史定位来看:
- KVC 不是给你写普通 app 的
- 它的目标是:让对象能被 跨语言、跨工具访问。
- AppleScript、Interface Builder、Core Data、Bindings(数据绑定)、甚至一些旧的 Cocoa 工具,全都依赖 KVC。
- setter 校验 vs validate 校验
- setter 校验:调用 setAge: 时能拦截无效值,没问题。
- KVC 校验:当外部用
setValue:forKey:
或者 UI 绑定框架(比如 Interface Builder 的 Cocoa Bindings)在更新属性前,就能调用validate<Key>:error:
来判断输入值是否合法。 - 好处是:UI 层可以在赋值前发现问题,不会直接 crash,也可以通过
NSError
把信息传回界面,弹个提示。
- 实际用途:数据绑定 (Cocoa Bindings)
- 在 macOS AppKit 里,有个很冷门但很强大的特性:Bindings。
- 比如一个文本框直接绑定到
person.age
,当用户输入值时,AppKit 会在背后调用validateAge:error:
来校验。 - 如果校验失败,UI 可以自动显示错误提示,而不是你手动写代码去监听输入变化再验证。
为什么感觉“没用”?
- 在 iOS 开发里,这套机制几乎没人用,因为 iOS 从来没有推行 Cocoa Bindings。
- 在 macOS 桌面应用(特别是老一代 AppKit + Interface Builder)里,确实有人用过这套机制来省掉 UI 验证逻辑。
- Apple 后来也逐渐弱化了 Bindings,转向更现代的 SwiftUI 数据流模型,所以这个东西在新代码里几乎成了遗迹。
所以结论是:
- ✅ 现代 iOS/Swift 开发者:完全不用管
validate<Key>:error:
,直接在 setter 或者数据模型层自己写验证就好。 - 🌀 历史原因:它主要是给 KVC + Bindings 体系设计的,让 UI 能在赋值之前通过统一协议进行验证。
- 📖 文档之所以写这么多:Apple 文档要保证完整性,告诉你 KVC 提供了这个 hook,但没告诉你“99% 的人不会用到”。
其实你这句话非常有代表性:
“直接在属性的 setter 方法里面对入参进行验证不比这脱了裤子放屁爽多了?”
这也是为什么 绝大多数 iOS/macOS 开发者对 validate<Key>
只停留在知道但不用的层面。
实现基本键值编码合规性要我做什么?
在大多数场景下,只要使用 @property 并让编译器自动生成 ivar 和访问器(accessor),就能自动满足 KVC(Key-Value Coding)要求,开发者无需手动实现绝大多数行为。只有在你的对象存在非对象属性的时候,必须重写 setNilValueForKey:
方法来避免可能的崩溃,实践中也建议在 setNilValueForKey:
内尽量使用 setValue:forKey:
,以保证 KVO 通知等机制能正确触发,而不是直接设值或设置 ivar 。
什么时候需要定义集合方法?
这一节的内容我相信也是很多人十分不理解的一节。集合不是有专门的 mutableXXXValueForKey:
这样的集合获取方法吗?然后就可以通过这个方法返回的结果,对集合属性进行任何增删改操作啊,还支持 KVO 功能。集合属性只需要通过 mutableXXXValueForKey:
不需要看起来也没有必要实现任何其他 KVC 的那些集合方法啊?
1. mutableXXXValueForKey:
的机制
- 调用
mutableArrayValueForKey:@"someProperty"
时,KVC runtime 会返回一个 特殊的代理对象(不是你原始的 NSMutableArray)。 - 这个代理对象拦截
addObject:
/removeObject:
等操作,转发给你的对象。 - 这样做的好处是:
- 自动触发 KVO 通知(观察者会知道集合变动了)。
- 统一封装逻辑(外部调用者不用关心你集合的真实存储结构)。
所以,如果你只是自己在 Swift/Objective-C 里操作集合,并且依赖 KVO,确实只需要用 mutableArrayValueForKey:
就够了。
2. Collection Methods(countOf<Key>
等)的作用
- 这些方法并不是为了你写代码时方便,而是为了 KVC runtime 能够在不同语言/上下文中自动把属性“当作集合”来使用。
- 举例:
- AppleScript 想访问
myObj.items[0]
,它不会直接知道 items 是一个 NSMutableArray。 - KVC runtime 会尝试调用
countOfItems
、objectInItemsAtIndex:
等方法,把你的对象包装成集合接口。 - 这样就算你内部不是用 NSArray 存储,而是用数据库、懒加载列表、Core Data faulting,KVC 依然能“装配”成一个可以下标访问的集合。
- AppleScript 想访问
- 换句话说:
mutableArrayValueForKey:
→ 是给 你主动获取可变集合代理 时用的。- Collection Methods → 是给 KVC runtime 在更通用/语言无关场景下 自动装配集合访问的。
3. 什么时候用哪个?
- 一般应用开发者(写 iOS / macOS app,用 ObjC/Swift)
- 不需要自己去写
countOf<Key> / objectIn<Key>AtIndex:
这种 Collection Methods。 - 只要集合属性真的是
NSMutableArray/NSMutableSet/NSMutableDictionary
,再通过mutableArrayValueForKey: / mutableSetValueForKey: / mutableDictionaryValueForKey:
拿代理,就能满足 增删改查 + 自动 KVO 的需求。 - 这是绝大多数开发者的场景。
- 不需要自己去写
- Apple 文档里强调的 Collection Methods
- 更偏向 框架作者 / 跨语言交互 / 特殊存储结构 的情况。
- 比如:
- 你内部集合不是用 NSMutableArray 存,而是懒加载、数据库查询、Core Data faulting 等。
- 或者你需要让 AppleScript / JavaScriptCore / KVC runtime 能把你的对象属性“包装成集合”。
- 这种情况下才需要你实现那一套方法,让 runtime 知道怎么装配出一个“集合语义”。
Apple 文档写得很“学术”,通篇把 KVC 的 accessor conventions 列出来,却没强调“普通开发者大多情况下用不到,直接用 mutableXXXValueForKey:
就行”。这也是 Apple 文档的一大毛病 —— 为了完整性,把所有机制罗列出来,但没有给读者一个“实用优先级”指引,让人误以为“是不是每个集合属性都要写一大堆方法”。
描述属性关系这一篇到底在讲什么东西?作为iOS应用开发者到底要干什么?
1. 这篇《描述属性关系》文档到底在讲什么?
它其实在讲:
- KVC 不仅仅能处理单纯的属性 (attribute),还可以处理对象之间的“关系 (relationship)”。
- “关系”分为:
- to-one:一对一,例如
person.employer
。 - to-many:一对多,例如
person.pets
。 - inverse:反向关系,例如 pet.owner 对应 person.pets。
- to-one:一对一,例如
- Cocoa 里有一个类
NSClassDescription
,它是用来描述这些“属性和关系”的元数据的。 - 框架(如 Core Data、AppleScript)会用
NSClassDescription
来告诉 KVC:某个 key 是普通属性,某个 key 是 to-one 或 to-many 关系,这样 KVC 才能更聪明地处理数据。
2. 作为 iOS 应用开发者要做什么?
👉 大部分情况下,你什么都不用做。
因为:
- 如果你在写 普通 App 业务代码,你根本不会直接用到
NSClassDescription
。 - 如果你在用 Core Data,Xcode 生成的模型文件已经帮你定义了关系(entity、attribute、to-one、to-many、inverse),Core Data 内部会用到
NSClassDescription
,但你不需要管。 - 如果你写 KVC/KVO,你只要关心
setValue:forKey:
、valueForKey:
、mutableArrayValueForKey:
这些就够了。关系的逻辑由框架实现。
3. 那文档为什么要写?
- 这是 Cocoa 框架的完整性设计文档,面向写框架的人(比如 Core Data、AppleScript 支持、ORM 框架开发者),而不是面向普通 App 开发者。
- Apple 为了完整性,会把最底层的“元数据机制”写清楚,但普通开发者 不会直接用。
4. 你需要怎么理解?
- 知道背景:KVC 不止能操作值,还能理解关系。
- 对日常开发的结论:
- 写普通 App:不用管,完全不会用到。
- 写 Core Data:关系已经在模型里声明了,你直接用 NSManagedObject 就好。
- 写底层框架/ORM(极少数情况):才可能接触
NSClassDescription
。
Apple 在这篇文档里一丁点说明都没有,然后还将它归类在 Adopting Key-Value Coding 章节下,让很多开发者误认为需要做些什么才能符合KVC规范。。。
这正是 Apple 文档里最“恶心”的地方:
- 他们把 框架设计层面的说明(比如
NSClassDescription
、关系描述、反向关系)和 应用开发者要用的 KVC 规范 混在一起写,- 还放在 Adopting Key-Value Coding 下面,看起来好像是“要想支持 KVC,就得自己实现这些关系方法”,但事实根本不是。
总结
整个 Key-Value Coding Programming Guide 文档里太多这样的误导文章了,几乎就是一篇屎一样东拼西凑出来的文档。。。
Apple 很多旧文档(尤其是 Programming Guide 系列)确实就是一坨拼凑物,问题集中在这几个方面:
- 写给框架作者和写给应用开发者混在一起
- 你只想知道“作为 iOS 开发者我要写啥代码”,结果文档里一堆 Core Data / AppleScript 的底层背景介绍,根本没说明白哪些是你需要做的,哪些是框架自己搞的。
- 就像你刚吐槽的 “描述属性关系” 一节,99% 开发者根本用不上。
- 没有明确上下文
- 文档一开头不说目标读者是谁,也不说“以下部分主要给框架作者看”,让人误以为“每一节都要掌握并实现”。
- 结果你读完一堆接口名、方法签名,完全不清楚到底是不是要自己写。
- 示例过时 / 脱离实际
- 很多示例还是 Mac OS X + AppleScript 年代的残留,放在今天的 iOS 开发语境里完全没用。
- 而且 Swift 生态里这些东西几乎都被替代了,但文档没更新,还是用 Objective-C 那套思路。
- 组织混乱,逻辑跳跃
- 有些地方本来应该按“日常用法 → 进阶用法 → 框架作者用法”来组织,但 Apple 文档常常是“先丢一堆 API,再扯点历史背景,再突然告诉你几个特殊方法”,让人越看越晕。
Apple 这类老文档最大的问题:
作为写给框架作者的文档,它也不合格
- 它只是列了几个方法名(
toOneRelationshipKeys
、inverseRelationshipKey:
…),告诉你有这么个类NSClassDescription
。 - 但没有任何:
- 完整的调用时机说明(什么时候 KVC 会去问
NSClassDescription
?) - 实际实现的示例(如何写一个子类?怎么注册?和 KVC 的调用链如何交互?)
- 和其他子系统的关系(Core Data 是怎么利用它的?AppleScript 是怎么依赖它的?)
- 完整的调用时机说明(什么时候 KVC 会去问
- 所以 —— 它给你的信息 既不足以让应用开发者知道要不要用,也 不足以让框架作者真正能靠它写东西。这就是所谓的“既不接地气,也不深入”的尴尬状态。
- 它只是列了几个方法名(
为什么会这样?
- 历史遗留:
这份 KVC Programming Guide 的主体是 OS X 10.1 ~ 10.3 时代写的,当时 AppleScript Bridge、Core Data 都刚起步。写文档的人更多是“描述愿景”,不是严谨的框架开发手册。 - 不维护:
后来 Swift 出来,KVC/KVO 渐渐成为“旧时代的动态特性”,文档就再没认真更新过。Apple 内部甚至可能都没人愿意重写。 - 定位模糊:
它夹在“应用开发文档”和“框架设计文档”之间,结果两边都没讨好。
- 历史遗留:
框架作者真正会看什么?
如果你真要基于 KVC 搞个 ORM / 数据绑定框架,你不会去看这篇文档,你会:- 看头文件:
NSKeyValueCoding.h
、NSClassDescription.h
。 - 看现成实现:
- Core Data 怎么实现 NSClassDescription 的子类。
- AppleScript Bridge 怎么注册类描述。
- 逆向调试调用链:看看
valueForKey:
在处理关系属性时,到底什么时候查询NSClassDescription
。
换句话说:真正写框架的人靠源码/调试,不靠这篇文档。
- 看头文件:
结论
- 作为应用开发文档 → 太复杂、误导。
- 作为框架开发文档 → 太皮毛、不够用。
所以它才会让人读了怒气值飙升 😅。本质就是一份“既不中用、又没写深”的垃圾拼凑物。
KVC 有关的面试题
通过 KVC 修改属性会触发 KVO 吗?
会的。
在绝大多数情况下,通过 KVC (setValue:forKey:
) 修改对象的属性时,会触发 KVO 通知。原因是:
setValue:forKey:
等 KVC 设置方法的实现逻辑(文档的访问器搜索逻辑中写了)会去调用属性对应的 setter 方法,而对象在被观察之后它的 setter 会被重写,内部会触发 KVO 通知。
特殊情况:手动关闭了自动 KVO。如果类中重写了 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
并返回 NO,那么即使通过 KVC 修改,也不会触发 KVO,除非你手动调用:
1 | [self willChangeValueForKey:@"name"]; |
总结:通过 KVC 修改属性 → 一般会触发 KVO(因为内部会走 setter)。除非你显示添加代码手动关闭了 KVO 通知。
KVC 的赋值和取值过程是怎样的?原理是什么?
呃,答案就在文档访问器搜索逻辑里。简单概括来说就是:
- KVC 的赋值
setValue:forKey:
等方法底层先去找各种相关 setter 方法,存在就直接调用,如果不存在则再去找各种相关实例变量,找到了就赋值,否则就会调用setValue:forUndefinedKey:
抛出异常。 - KVC 的取值
valueForKey:
等方法的底层先去找各种相关的 getter 方法,找到了就直接调用,如果不存在则再去找各种相关的实例变量,找到了就返回值,否则就调用valueForUndefinedKey:
抛出异常。这只是概括,具体的取值逻辑中还有 NSArray 和 NSSet 的集合访问器逻辑。