Swift 与 Objective-C

[Swift 哲学 #1] Safe・Fast・Expressive

使用 Swift 编写代码时,你总会产生一些疑问:为什么可选值这么严格?为什么数组会被复制?为什么会有 guard?逐一深入这些问题,最终会遇到 Swift 从一开始就提出的三个目标:Safe(安全)、Fast(快速)、Expressive(富有表现力)。

8 分钟阅读
[Swift 哲学 #1] Safe・Fast・Expressive 封面图

使用 Swift 编写代码时,你总会产生一些疑问:为什么可选值这么严格?为什么数组会被复制?为什么会有 guard?逐一深入这些问题,最终会遇到 Swift 从一开始就提出的三个目标:Safe(安全)、Fast(快速)、Expressive(富有表现力)。

Swift 官方网站(swift.org)在语言介绍开头明确列出了这三点。这不只是营销文案。过去十多年加入 Swift 的几乎所有功能,都经过了这三个标准的检验。本文将总结三种哲学分别如何落实为语言功能,以及三者发生冲突时 Swift 选择了哪一方。

本文是 Swift 哲学系列的第一篇。Swift 的诞生背景,也就是 Chris Lattner 为什么放弃 Objective-C,已在另一篇文章中介绍;这里将重点放在 Swift 诞生后遵循什么原则成长。

Safe — 让编译器提前阻止错误

安全是 Swift 设计中优先级最高的价值。这里的安全,指的是“让犯错变得困难”。Swift 不依赖程序员的善意或专注力,而是在语言层面堵住犯错的途径。

代表性机制是可选值。在 Objective-C 时代,任何指针都可能是 nil,向 nil 发送消息会被静默忽略。虽然不会崩溃,却很难追踪 bug 从何处开始。C 系语言则会直接崩溃。Tony Hoare 将 null 引用称为“价值十亿美元的错误”,这是众所周知的故事。

Swift 将这个问题提升到了类型系统层面。可能没有值的变量必须在类型中加问号声明,例如 String?;没有问号的类型绝不可能是 nil。要使用可能为 nil 的值,编译器会强制你通过 if let 或 guard let 将其解包。

var name: String? = fetchUserName()

// 编译错误 — 不能直接使用可选值
// print(name.count)

if let name {
    print(name.count) // 这里已保证安全
}

运行时 bug“忘记检查 nil”,变成了编译错误“没有解包可选值”。bug 会在构建阶段被拦截,不会到达用户手中。

除了可选值,安全哲学还体现在许多地方。

  • 强制变量在使用前初始化:从源头阻止读取未初始化内存的 bug。
  • 数组边界检查:索引越界时,Swift 会立即停止,而不是读取异常内存。
  • 检测整数溢出:C 会静默地让数值翻转,而 Swift 会在普通运算中触发陷阱。
  • 有类型推断,但没有隐式转换:不能直接将 IntDouble 相加。虽然麻烦,但考虑到 C 的隐式类型转换会造成微妙 bug,这样做是值得的。

这里有一点值得注意。Swift 的安全更接近于“没有未定义行为”,而不是“不会崩溃”。数组越界时,Swift 反而会故意崩溃。与其带着奇怪的值继续执行,不如在问题点可靠地停止,这更安全。

Swift 的安全哲学会在编译关卡提前拦截运行时 bug
Swift 的安全哲学会在编译关卡提前拦截运行时 bug

Fast — 不为安全牺牲性能

安全的语言很多。问题在于安全机制通常代价高昂:垃圾回收、运行时类型检查、解释器。脚本语言一直用速度换取安全和便利。

Swift 的野心是拒绝这笔交易本身。目标很明确:保留上述全部安全机制,同时达到接近 C 系语言的性能。为此,Swift 铺设了多层机制。

第一,尽可能在编译时确定。Swift 是静态类型语言,编译器知道所有类型,因此可以在编译时将方法调用直接固定为地址(静态派发)。这与 Objective-C 在运行时通过 objc_msgSend 查找所有方法调用形成对比。类加上 final 后能获得更积极的优化,也是同一原理。

第二,使用 ARC(Automatic Reference Counting,自动引用计数)代替垃圾回收。引用计数会在编译时插入 retain/release 代码,因此不像 GC(垃圾回收)那样在运行时暂停程序并清理内存。能够预测内存何时释放也是一项优势。

第三,值类型和以协议为中心的设计。struct 无需承担堆分配和引用计数成本即可放在栈上;泛型经过特化后,会编译成针对各类型的专用代码。使用抽象并不意味着支付运行时成本,编译器会去除抽象并生成具体代码。这称为零成本抽象。

当然,现实比目标复杂。Swift 也存在隐藏的性能成本,例如存放协议类型的 existential container,以及类的引用计数开销。因此,准确的理解不是“Swift 一定很快”,而是“语言为高性能留下了道路,偏离这条道路就要付出代价”。后续深入篇将详细讨论这条道路。

Expressive — 让意图直接呈现在代码中

三种价值中最难把握的是表现力。简单来说,读代码时应该直接理解作者的意图,并且无需冗长的仪式就能写出想表达的内容。

与 Objective-C 对比时,这种差异尤其明显。

// Objective-C
NSArray *names = @[@"Kim", @"Lee", @"Park"];
NSMutableArray *upper = [NSMutableArray array];
for (NSString *name in names) {
    [upper addObject:[name uppercaseString]];
}
// Swift
let names = ["Kim", "Lee", "Park"]
let upper = names.map { $0.uppercased() }

行数减少当然重要,但更重要的是意图密度。一个 map 就完整表达了“转换每个元素并创建新数组”的意图。for 循环版本则需要读者自行重建这种意图。

支持表现力的机制有很多,举几个例子:

  • 类型推断:写成 let names = ["Kim", "Lee"] 时,编译器知道它是 [String]。保留类型安全,只减少类型标注的噪音。
  • enum 和关联值:将“成功时是数据、失败时是错误”的状态直接建模为 case success(Data)case failure(Error),让状态与数据不会分离。
  • 尾随闭包、下标和运算符定义:让库能够创建读起来像语言语法的 API。
  • resultBuilder:SwiftUI 的声明式语法就是用它构建的,UI 结构会与代码结构一致。

也有需要注意的地方。表现力不等于“短”。Swift API Design Guidelines 的第一原则是“使用位置的清晰度(clarity at the point of use)”,清晰度优先于简洁。这就是为什么存在像 remove(at: 3) 这样刻意添加参数标签的语法。remove(3) 更短,但读者会困惑它删除的是第三个位置,还是值 3。

发生冲突时安全胜出,性能和表现力由编译器收回
发生冲突时安全胜出,性能和表现力由编译器收回

三者冲突时,谁会胜出

有三种哲学,就必然会出现冲突。Swift 的真正性格,就体现在这些冲突的裁决记录中。

安全 vs 表现力:可选值解包确实会让代码更嘈杂。如果像 Python 一样直接使用,代码会更短。Swift 选择了安全,并通过 if let 简写语法、可选链(user?.name)、nil 合并(??)等,在保持安全的同时减少噪音,以此补偿表现力。

安全 vs 性能:数组边界检查意味着每次访问都要增加比较操作。Swift 默认选择安全,但当编译器能够证明不可能越界时,会通过移除检查来收回性能。对于确实需要的人,它也提供了 withUnsafeBufferPointer 这样的逃生口。名称中写入 unsafe,让承担风险这一事实留在代码中。

性能 vs 表现力:高阶函数和泛型等抽象是表现力的核心,但朴素实现会很慢。Swift 通过内联和泛型特化投入编译器能力,让“即使使用抽象,也能生成与手写代码相同的机器码”。

看到这个模式了吗?默认值永远是安全;性能和表现力则通过编译器优化与显式逃生口收回。Swift 几乎所有设计决策都可以用这个公式解释。

这一哲学在实践中的意义

哲学听起来可能很抽象,但它与实际开发有直接联系。

第一,与编译器合作,而不是对抗它,更有益。在 Swift 中,大多数编译错误都是“现在捕获了未来的运行时 bug”的信号。因为可选值麻烦就滥用 !,等于亲手拆掉语言建立的防线。

第二,设计 API 时有了判断标准。如果自己编写的函数很容易被误用,就不够 Swift。设计类型,让错误用法变成编译错误,这是 Swift 风格的核心,也是后续系列会反复遇到的主题。

第三,新功能更容易理解。async/await 同时针对回调地狱这一表现力问题和数据竞争这一安全问题;Swift 6 的 strict concurrency 则延续了“连并发 bug 也要在编译时捕获”的安全哲学。宏在编译时解决样板代码这一表现力问题。了解三种哲学后,每当新功能出现,就能看出它推动了哪种价值、采用了什么方式。

总结

  • Swift 的所有设计,都源自 Safe、Fast、Expressive 三种价值之间的平衡。
  • Safe:可选值、强制初始化、边界检查,把错误变成编译错误,而不是运行时 bug。
  • Fast:静态派发、ARC、值类型、泛型特化,在保留安全机制的同时追求 C 级性能。
  • Expressive:类型推断、enum、闭包,目标不是短,而是意图清晰。
  • 发生冲突时默认选择安全,性能和表现力则通过编译器优化与显式逃生口收回。

下一篇将讨论这一哲学如何反映在学习曲线上:一行 print 脚本与泛型库能够共存于同一语言的秘密——Progressive Disclosure。

延伸阅读