我不知道有多少人像本人一样,n 年来查看过不知道多少次这个 KVC 文档,但是每次通篇读下来总是会遇到读不懂的地方,总是莫名其妙的出现一个章节完全不知道在讲什么东西,不论是用传统的 Google 翻译或者目前主流的 AI 翻译翻译了,也还是会遇到读起来狗屁不通的情况。

  • 其实根本原因是原始文档就有问题,东拼西凑的一坨屎,有歧义,导致不论你是直译还是意译都是让人摸不着头脑的翻译,只能根据上下文的理解才能正确翻译。为此作者将这份文档结合AI和个人理解做了全篇翻译,有需要的可以自行下载
  • 还有原始文档很多地方只写了要怎么做,但是没有告诉你为什么要这么做,就会让人觉得莫名其妙。
  • 文档的有些内容太古老了,某些内容只在早期的 MacOS 开发中使用,在 iOS 开发中几乎接触,使用不到,但是也没有做任何说明。

这次作者决定结合 AI 和个人的理解将 KVC 文档彻底弄清楚,即使不能完全弄明白也要留下一个深刻的印象,方便后续遇到文章中的场景时能够回想起来。


章节概览

✅ 建议阅读
❌ 不建议阅读

入门指南

  1. ✅ 关于键值编码

键值编码基础知识

  1. ✅ 访问对象的属性
  2. ✅ 访问集合属性
  3. ✅ 使用集合操作符
  4. ✅ 表示非对象值
  5. ❌ 验证属性
  6. ✅ 访问器搜索逻辑

采用键值编码协议

  1. ❌ 实现基本键值编码合规性
  2. ❌ 定义集合方法
  3. ❌ 处理非对象值
  4. ❌ 添加验证
  5. ❌ 描述属性关系
  6. ❌ 以性能为导向进行设计
  7. ❌ 合规性检查清单

常见疑惑及解答

以下是作者对这篇文档的一些疑问以及结合 AI 的回答。

属性验证是什么鬼?

Person 有一个 NSString *name 属性,如果需要给 name 添加验证则在内部添加以下代码

1
2
3
4
5
6
7
8
9
10
11
12
- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
if (outError != NULL) {
*outError = [NSError errorWithDomain:PersonErrorDomain
code:PersonInvalidNameCode
userInfo:@{ NSLocalizedDescriptionKey
: @"Name too short" }];
}
return NO;
}
return YES;
}

在外部使用属性验证:

1
2
3
4
5
6
Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
NSLog(@"%@",error);
}

谁会用到这么奇葩的功能?直接在 setter 方法里面对入参进行验证就非常合理了啊。为什么还要这么脱了裤子放屁?

上面的例子就是 KVC 的属性验证机制(validate:error:),很多人第一次看到的时候都会觉得:「这玩意儿不是多此一举吗?我直接在 setter 里校验不就好了?」

为什么会有 validate:error: 这种“奇葩”设计?

要理解它,就要从 KVC/KVO 的历史定位来看:

  1. KVC 不是给你写普通 app 的
    • 它的目标是:让对象能被 跨语言、跨工具访问。
    • AppleScript、Interface Builder、Core Data、Bindings(数据绑定)、甚至一些旧的 Cocoa 工具,全都依赖 KVC。
  2. setter 校验 vs validate 校验
    • setter 校验:调用 setAge: 时能拦截无效值,没问题。
    • KVC 校验:当外部用 setValue:forKey: 或者 UI 绑定框架(比如 Interface Builder 的 Cocoa Bindings)在更新属性前,就能调用 validate<Key>:error: 来判断输入值是否合法。
    • 好处是:UI 层可以在赋值前发现问题,不会直接 crash,也可以通过 NSError 把信息传回界面,弹个提示。
  3. 实际用途:数据绑定 (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 会尝试调用 countOfItemsobjectInItemsAtIndex: 等方法,把你的对象包装成集合接口。
    • 这样就算你内部不是用 NSArray 存储,而是用数据库、懒加载列表、Core Data faulting,KVC 依然能“装配”成一个可以下标访问的集合。
  • 换句话说:
    • 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。
  • 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 系列)确实就是一坨拼凑物,问题集中在这几个方面:

  1. 写给框架作者和写给应用开发者混在一起
    • 你只想知道“作为 iOS 开发者我要写啥代码”,结果文档里一堆 Core Data / AppleScript 的底层背景介绍,根本没说明白哪些是你需要做的,哪些是框架自己搞的。
    • 就像你刚吐槽的 “描述属性关系” 一节,99% 开发者根本用不上。
  2. 没有明确上下文
    • 文档一开头不说目标读者是谁,也不说“以下部分主要给框架作者看”,让人误以为“每一节都要掌握并实现”。
    • 结果你读完一堆接口名、方法签名,完全不清楚到底是不是要自己写。
  3. 示例过时 / 脱离实际
    • 很多示例还是 Mac OS X + AppleScript 年代的残留,放在今天的 iOS 开发语境里完全没用。
    • 而且 Swift 生态里这些东西几乎都被替代了,但文档没更新,还是用 Objective-C 那套思路。
  4. 组织混乱,逻辑跳跃
    • 有些地方本来应该按“日常用法 → 进阶用法 → 框架作者用法”来组织,但 Apple 文档常常是“先丢一堆 API,再扯点历史背景,再突然告诉你几个特殊方法”,让人越看越晕。

Apple 这类老文档最大的问题:

  1. 作为写给框架作者的文档,它也不合格

    • 它只是列了几个方法名(toOneRelationshipKeysinverseRelationshipKey:…),告诉你有这么个类 NSClassDescription
    • 但没有任何:
      • 完整的调用时机说明(什么时候 KVC 会去问 NSClassDescription?)
      • 实际实现的示例(如何写一个子类?怎么注册?和 KVC 的调用链如何交互?)
      • 和其他子系统的关系(Core Data 是怎么利用它的?AppleScript 是怎么依赖它的?)
    • 所以 —— 它给你的信息 既不足以让应用开发者知道要不要用,也 不足以让框架作者真正能靠它写东西。这就是所谓的“既不接地气,也不深入”的尴尬状态。
  2. 为什么会这样?

    • 历史遗留:
      这份 KVC Programming Guide 的主体是 OS X 10.1 ~ 10.3 时代写的,当时 AppleScript Bridge、Core Data 都刚起步。写文档的人更多是“描述愿景”,不是严谨的框架开发手册。
    • 不维护:
      后来 Swift 出来,KVC/KVO 渐渐成为“旧时代的动态特性”,文档就再没认真更新过。Apple 内部甚至可能都没人愿意重写。
    • 定位模糊:
      它夹在“应用开发文档”和“框架设计文档”之间,结果两边都没讨好。
  3. 框架作者真正会看什么?
    如果你真要基于 KVC 搞个 ORM / 数据绑定框架,你不会去看这篇文档,你会:

    • 看头文件:NSKeyValueCoding.hNSClassDescription.h
    • 看现成实现:
      • Core Data 怎么实现 NSClassDescription 的子类。
      • AppleScript Bridge 怎么注册类描述。
    • 逆向调试调用链:看看 valueForKey: 在处理关系属性时,到底什么时候查询 NSClassDescription

    换句话说:真正写框架的人靠源码/调试,不靠这篇文档。

  4. 结论

    • 作为应用开发文档 → 太复杂、误导。
    • 作为框架开发文档 → 太皮毛、不够用。

所以它才会让人读了怒气值飙升 😅。本质就是一份“既不中用、又没写深”的垃圾拼凑物。

KVC 有关的面试题

通过 KVC 修改属性会触发 KVO 吗?

会的。

在绝大多数情况下,通过 KVC (setValue:forKey:) 修改对象的属性时,会触发 KVO 通知。原因是:

setValue:forKey: 等 KVC 设置方法的实现逻辑(文档的访问器搜索逻辑中写了)会去调用属性对应的 setter 方法,而对象在被观察之后它的 setter 会被重写,内部会触发 KVO 通知。

特殊情况:手动关闭了自动 KVO。如果类中重写了 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key 并返回 NO,那么即使通过 KVC 修改,也不会触发 KVO,除非你手动调用:

1
2
3
[self willChangeValueForKey:@"name"];
_name = newValue;
[self didChangeValueForKey:@"name"];

总结:通过 KVC 修改属性 → 一般会触发 KVO(因为内部会走 setter)。除非你显示添加代码手动关闭了 KVO 通知。

KVC 的赋值和取值过程是怎样的?原理是什么?

呃,答案就在文档访问器搜索逻辑里。简单概括来说就是:

  • KVC 的赋值 setValue:forKey: 等方法底层先去找各种相关 setter 方法,存在就直接调用,如果不存在则再去找各种相关实例变量,找到了就赋值,否则就会调用 setValue:forUndefinedKey: 抛出异常。
  • KVC 的取值 valueForKey: 等方法的底层先去找各种相关的 getter 方法,找到了就直接调用,如果不存在则再去找各种相关的实例变量,找到了就返回值,否则就调用 valueForUndefinedKey: 抛出异常。这只是概括,具体的取值逻辑中还有 NSArray 和 NSSet 的集合访问器逻辑。