关于 Hello Swift

**最好的学习方法是间隔性重复学习。**

一个编程高手是怎样练成的呢? 惟手熟尔。重在刻意练习。 这意为着,就是不断重复练习,实践,再实践,熟练掌握各种技能。因为,只有反复练习,才能真正掌握。

Hello,Swift 是如何产生的呢? 这是我在学习 Swift 过程中,不断地编写样例代码,不断点滴积累经验,最终形成的。

Swift 是一个非常优秀的现代编程语言,它简洁易读,性能高,安全,功能强大。然而,它也存在一些独特的概念需要理解,比如可选类型 (Optional)、面向协议编程 (POP)、值类型语义等,对于从其他语言迁移的开发者来说,需要花费一定时间去适应。

对于新手来说,Hello, Swift 是一个绝佳的起点。通过这个项目,你不仅能快速入门 Swift 编程,还能通过编程、调试、运行示例代码,迅速掌握 Swift 的核心知识点,熟悉基础语法和基本概念。更棒的是,它还涵盖了高级进阶知识和 Swift 6.0 并发编程等现代特性。

本书的当前版本假设你使用 Swift 6.0 或更高版本。Swift 6.0 引入了 Strict Concurrency 模式,在编译时检测数据竞争 (Data Race),让并发编程更加安全。请查看快速开始的"安装"部分了解如何安装和升级 Swift。


Hello Swift 的特点

Hello, Swift 教程具有以下特点:

  • 中文优先:所有教程内容以中文为主,Swift 专有术语附带英文对照(如:可选类型 (Optional)、协议 (Protocol)),便于理解和技术交流。
  • 代码驱动:每个章节都有真实可运行的代码示例,来源于 Sources/BasicSample/ 目录,所有示例都经过编译验证。
  • 循序渐进:从变量与表达式开始,逐步深入到类型系统、并发编程等高级主题,形成完整的学习路径。
  • 对比学习:每个章节都包含 Swift 与 Rust/Python 的对比速查表,帮助你从已有语言经验快速迁移。
  • 实战导向:不仅是语法讲解,还包含工业界应用场景、常见错误排查、动手练习和知识检查。

适合谁阅读?

  • 编程新手:没有任何 Swift 经验,想从零开始学习现代编程语言。
  • 有其他语言经验的开发者:熟悉 Python、JavaScript、Java、Rust 等语言,想快速掌握 Swift。
  • iOS/macOS 开发者:想深入理解 Swift 语言本身,为应用开发打下坚实基础。
  • 从 hello-rust 迁移的学习者:已经熟悉 hello-rust 的教程风格,想用同样的方式学习 Swift。

如何使用本书?

  1. 按顺序阅读:章节之间有依赖关系,建议从变量与表达式开始,依次学习。
  2. 动手实践:每章都有代码示例和练习,请务必在本地运行和修改代码。
  3. 对比学习:如果你熟悉其他语言,重点阅读 Swift 与 Rust/Python 对比表。
  4. 自我检测:每章末尾有知识检查题,检验你的理解程度。
  5. 回顾总结:完成基础部分后,阅读阶段复习巩固知识。

准备好了吗?让我们从快速开始开始你的 Swift 学习之旅!

简介

Swift 是一种现代、高性能的编程语言,专注于安全性、表达力和速度。由 Apple 开发,广泛应用于 iOS/macOS 应用开发、系统编程和高性能应用。

Swift 是一种通用的编程语言,强调性能、类型安全和并发性。它强制执行内存安全,通过可选类型 (Optional) 明确处理空值。与传统的垃圾回收机制不同,Swift 使用 ARC (Automatic Reference Counting) 自动管理内存,在编译时通过严格并发检查 (Strict Concurrency) 防止数据竞争。

Swift 支持多种编程范式。它深受函数式编程思想影响,包括不可变性、高阶函数、代数数据类型和模式匹配。同时,它通过结构体 (Struct)、类 (Class)、枚举 (Enum) 和协议 (Protocol) 支持面向对象编程和面向协议编程 (POP)。


为什么选择 Swift?

  • 安全性:Swift 的可选类型 (Optional) 明确处理空值,避免空指针异常。Swift 6.0 的严格并发检查 (Strict Concurrency) 在编译时检测数据竞争,确保并发安全。
  • 性能:Swift 通过值类型语义 (Value Type) 和 ARC 内存管理实现高性能。Swift 编译器生成优化的机器码,接近 C 的性能,同时保持现代语言的安全特性。
  • 并发性:Swift 6.0 提供强大的并发模型,支持 async/await、Actor、Task 等异步原语,让并发编程既安全又简洁。
  • 优秀的包管理:Swift Package Manager (SPM) 提供强大的包管理工具,包版本、依赖、测试一站式解决,体验便捷。
  • 可读性:Swift 的代码风格统一简洁易读。命名清晰,API 设计指南确保一致性。
  • 面向协议编程 (POP):Swift 的协议 (Protocol) + 扩展模式提供比继承更灵活的代码复用方式,是 Swift 最独特的设计理念。

Swift vs Rust 对比

特性SwiftRust
内存管理ARC (自动引用计数)所有权系统 (Ownership)
空值处理Optional (? / if let)Option<T>
默认可变性let 不可变,var 可变let 不可变,let mut 可变
类型系统值类型 + 引用类型只有值类型 (结构体)
抽象方式协议 (Protocol) + POPTrait + 泛型
并发模型async/await + Actorasync/await + Future
错误处理throws / do-catch-tryResult<T, E> / ? 操作符

Swift 的独特优势

面向协议编程 (POP):Swift 的协议可以定义方法、属性、关联类型,并通过协议扩展提供默认实现。这比传统的类继承更灵活,避免了继承带来的耦合问题。

值类型优先:Swift 默认使用值类型 (结构体、枚举),赋值时复制而非引用。这让代码更容易理解,减少意外的数据共享问题,天然适合并发场景。

互操作性:Swift 可以无缝调用 C 和 Objective-C 代码,利用现有生态系统。与 Apple 平台深度集成,是 iOS/macOS 开发的首选语言。

现代语法:Swift 的语法简洁现代,减少样板代码。字符串插值、尾随闭包、属性观察器等特性让代码更清晰。


学习 Swift 的挑战

Swift 有一些独特概念需要理解:

  • 可选类型 (Optional):明确处理空值,需要理解解包方式 (if let, guard let, !)
  • 值类型 vs 引用类型:何时用 struct,何时用 class
  • 面向协议编程 (POP):与传统 OOP 思维不同
  • 并发模型:Swift 6.0 的 Actor、Sendable、async/await
  • 闭包捕获:理解逃逸闭包、捕获值

这些概念一旦理解,会让你写出更安全、更优雅的代码。


准备好了吗?让我们开始 Swift 学习之旅!

快速开始

环境准备

  • 安装 Swift 6.0+ 工具链:
    • macOS:通过 Xcode 16+ 安装(包含 Swift 6.0)
    • Linux/Windows:从 Swift 官网 下载对应版本
  • 安装 Git
  • (可选)安装 mdBook 用于本地预览文档

克隆仓库

git clone https://github.com/savechina/hello-swift.git
cd hello-swift

构建项目

使用 Swift Package Manager 构建:

swift build

运行 CLI 工具

项目提供 hello 命令行工具(基于 swift-argument-parser):

# 查看所有可用子命令
swift run hello --help

# 运行不同模块示例
swift run hello basic    # 基础 Swift 语法示例
swift run hello advance  # 高级 Swift 特性示例
swift run hello awesome  # 第三方库集成示例
swift run hello algo     # 算法示例

运行测试

swift test

# 运行指定测试模块
swift test --filter HelloSampleTests

本地预览文档

教程文档使用 mdBook 构建:

# 安装 mdBook(如果未安装)
cargo install mdbook

# 启动本地服务,访问 http://localhost:3000
mdbook serve Docs/

项目结构概览

hello-swift/
├── Sources/
│   ├── HelloSample/        # CLI 入口(@main,ArgumentParser)
│   ├── BasicSample/        # 12 个基础 Swift 语法示例
│   └── AlgoSample/         # 算法实现
├── AdvanceSample/           # 嵌套 SPM 包(SwiftyJSON、swift-nio、dotenv)
├── AwesomeSample/           # 嵌套 SPM 包(第三方集成示例)
├── LeetCodeSample/         # 嵌套 SPM 包(LeetCode 题解)
├── Docs/                   # mdBook 教程源码(Docs/src/)和生成结果(Docs/book/)
├── Tests/                  # XCTest 测试套件
└── Package.swift           # 根 SPM 配置(Swift 6.0)

下一步

Basic 基础部分

📖 学习内容概览

欢迎来到 Swift 编程之旅的第一站!基础部分将带你掌握 Swift 的核心概念和编程范式。这些知识是后续高级主题和实战应用的基石。


🎯 你将学到什么

完成本部分学习后,你将能够:

  1. 理解 Swift 类型系统 - 结构体、类、枚举、协议的核心区别
  2. 掌握值类型与引用类型 - Swift 的默认值语义
  3. 使用面向协议编程 (POP) - Swift 最独特的设计理念
  4. 编写泛型代码 - 可复用的类型安全代码
  5. 理解错误处理 - do/catch/try 最佳实践

📚 基础部分导航

本部分涵盖以下 12 个章节:

#章节说明难度预计时间
1变量与表达式let/var、类型推断、字符串插值🟢 简单30 分钟
2基础数据类型Int、Double、Bool、String、集合类型、可选类型🟢 简单45 分钟
3控制流if/else、switch/case、for-in、guard、while🟢 简单30 分钟
4函数参数标签、默认值、可变参数、函数类型、嵌套函数🟡 中等45 分钟
5枚举定义、关联值、原始值、递归枚举🟡 中等45 分钟
6结构体定义、属性观察器、值类型语义、mutating🟡 中等45 分钟
7类与对象继承、初始化、ARC、类型转换🟡 中等60 分钟
8协议定义、扩展、关联类型、POP 设计理念🟡 中等60 分钟
9泛型泛型函数、类型约束、where 子句🟡 中等45 分钟
10错误处理do/catch/try、throws、defer、Result 类型🟡 中等45 分钟
11闭包闭包语法、捕获值、逃逸闭包、高阶函数🟡 中等45 分钟
12并发编程async/await、Actor、Task、Sendable🔴 困难60 分钟

总计: 12 章 | 总时间: 约 8 小时 15 分钟


🔗 前置要求

无需 Swift 基础! 本部分从零开始教学。

建议具备:

  • 基本编程概念(变量、循环、函数)
  • 任意编程语言经验(Python、JavaScript、Java、Rust 等)
  • 如果你熟悉 Hello Rust 的教程风格,会发现结构一致

📈 学习路径

变量与表达式 → 基础数据类型 → 控制流 → 函数 → 枚举 → 结构体 → 类与对象 → 协议 → 泛型 → 错误处理 → 闭包 → 并发编程

🎓 Swift 核心特性速览

Swift 区别于其他编程语言的核心特性:

特性说明与 Rust 对比
默认不可变 (let)变量默认不可变,需要 var 才可变与 Rust 一致 (let vs let mut)
值类型优先 (struct)默认使用值类型,类用于特殊场景Rust 只有结构体,没有类
面向协议编程 (POP)用协议+扩展替代类继承Rust 的 trait 类似但无默认实现
可选类型 (Optional)? 处理空值的类型安全机制Rust 的 Option<T>
async/await语言级并发支持Rust 异步基于 Future trait
ARC 内存管理自动引用计数Rust 的所有权系统

✅ 学习检查点

完成本部分后,你应该能够:

  • 使用 letvar 正确声明变量
  • 使用可选类型安全地处理空值
  • 使用枚举+关联值建模多状态数据
  • 在结构体和类之间做出正确选择
  • 定义协议并使用扩展提供默认实现
  • 编写泛型函数和泛型类型
  • 使用 do/catch/try 处理错误
  • 使用闭包编写函数式代码
  • 使用 async/await 编写并发代码
  • 理解 Sendable 和 Actor 隔离

🎓 实践项目

建议练习:

  1. 创建一个简单的命令行计算器(变量、函数、控制流)
  2. 实现一个联系人管理工具(结构体、类、集合类型)
  3. 编写一个异步网络数据获取程序(并发编程、错误处理)

➡️ 下一步

完成基础部分后,继续学习 高级进阶 部分,你将学习:

  • JSON 处理(JSONSerialization、Codable、SwiftyJSON)
  • 文件操作(FileManager、临时文件)
  • 系统服务(网络可达性、系统配置)
  • 异步编程(Task、SwiftNIO 集成)
  • 环境配置(swift-dotenv)

准备好了吗?让我们开始 变量与表达式 的学习! 🚀

变量与表达式

开篇故事

想象你有一个工具箱,里面装着各种工具:螺丝刀、锤子、尺子。你给每个工具贴上标签,下一次需要时就知道去哪里找。Swift 中的变量就像这些贴标签的工具箱 - 它们帮你存储和管理程序中的数据。表达式则是你使用这些工具完成的工作。

在 Swift 中,变量声明有一个非常特别的设计:默认情况下,所有变量都是不可变的。这就像你写在纸上的数字 - 写完后就不能改变。如果你想改,需要重新写一张纸。这个设计让代码更安全、更容易理解。


本章适合谁

如果你是 Swift 初学者,想理解如何存储数据、进行计算和控制程序流程,本章适合你。这是所有编程的基础,即使你是第一次接触编程也能理解。


你会学到什么

完成本章后,你可以:

  1. 使用 let 关键字声明不可变变量
  2. 使用 var 关键字声明可变变量
  3. 理解 Swift 类型推断机制
  4. 使用字符串插值构建动态文本
  5. 区分常量 (let) 和变量 (var) 的使用场景

前置要求

本章是 Swift 的第一章,不需要任何前置知识。


第一个例子

让我们从最简单的变量声明开始。打开 Sources/BasicSample/ExpressionSample.swift,找到以下代码:

let name = "Swift"
let version = "6.0"
print("Hello, \(name) \(version)!")

发生了什么?

  • let name = "Swift" - 声明一个不可变常量,值为 "Swift"
  • let version = "6.0" - 声明另一个常量
  • \(name) - 字符串插值,将变量值嵌入字符串

输出:

Hello, Swift 6.0!

原理解析

1. 不可变变量 (let)

Swift 默认让变量不可变:

let x = 5
// x = 6  // ❌ 编译错误!x 是不可变的

为什么 Swift 要这样设计?

  1. 安全性:防止意外的数据修改
  2. 并发安全:不可变数据可以安全地在线程间共享
  3. 编译器优化:不可变值让编译器能做更多优化

类比

就像你写在纸上的数字 - 写完后就不能改变。如果你想改,需要拿一张新纸重写。

2. 可变变量 (var)

当你需要修改变量时,使用 var

var counter = 0
counter = 1  // ✅ 可以修改
counter += 1 // ✅ 也可以这样累加

注意:只在需要修改时使用 var,这是 Swift 的最佳实践。

3. 类型推断 vs 显式类型

Swift 会自动推断类型:

let inferred = 42        // Int
let explicit: Double = 3.14  // 显式指定 Double
let message = "Hello"    // String

何时需要显式类型?

  • 编译器无法推断时
  • 你想覆盖默认推断(如 IntDouble
  • 提高代码可读性

4. 字符串插值

let name = "Alice"
let age = 30
print("\(name) is \(age) years old")
// 输出: Alice is 30 years old

// 插值中可以写表达式
print("Next year, \(name) will be \(age + 1)")
// 输出: Next year, Alice will be 31

5. 变量遮蔽 (Shadowing)

Swift 允许在嵌套作用域中重新声明同名变量:

let x = 5
if true {
    let x = 10  // 新 x 遮蔽了旧 x
    print("Inside: \(x)")  // 10
}
print("Outside: \(x)")  // 5

常见错误

错误 1: 试图修改 let 声明的常量

let maxCount = 100
maxCount = 200 // ❌ 编译错误!

编译器输出:

error: cannot assign to value: 'maxCount' is a 'let' constant

修复方法

var maxCount = 100
maxCount = 200 // ✅ 使用 var

错误 2: 类型不匹配

let number: Int = 3.14 // ❌ 编译错误!

编译器输出:

error: cannot convert value of type 'Double' to specified type 'Int'

修复方法

let number: Double = 3.14 // ✅ 类型匹配

错误 3: 未初始化就使用

let value: Int
print(value) // ❌ 编译错误!

修复方法

let value: Int = 0 // ✅ 声明时初始化

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
不可变声明无(约定用大写)let x = 5let x = 5Swift/Rust 语法一致
可变声明x = 5 (总是可变)let mut x = 5var x = 5Swift 用 var,Rust 用 mut
类型注解x: int = 5let x: i32 = 5let x: Int = 5Swift 首字母大写
字符串插值f"{x}"format!("{x}")"\(x)"Swift 用 \(x)
常量关键字const (编译时)let (运行时)Swift let 是运行时常量

动手练习

练习 1: 预测输出

不运行代码,预测下面代码的输出:

let x = 5
let y = x + 3
print("y = \(y)")
点击查看答案

输出:

y = 8

解析:

  1. x = 5 - 声明常量 x
  2. y = x + 3 - x + 3 = 8
  3. 字符串插值输出

练习 2: 修复错误

下面的代码有编译错误,请修复:

let counter = 0
counter = counter + 1
print("Counter: \(counter)")
点击查看修复方法

修复:

var counter = 0  // 改用 var
counter = counter + 1
print("Counter: \(counter)")

原因: let 声明的常量不能被修改,需要使用 var

练习 3: 字符串插值

使用字符串插值,打印以下信息:

  • 你的名字
  • 你的年龄
  • 明年你的年龄
点击查看参考实现
let name = "Alice"
let age = 25
print("\(name) is \(age) years old. Next year, \(name) will be \(age + 1).")

故障排查 FAQ

Q: 什么时候应该使用 let,什么时候应该使用 var

A: 遵循这个原则:

  • 默认使用 let - 99% 的情况不需要修改
  • 需要修改时使用 var - 计数器、累加器、状态标志
  • 可以重新声明时优先遮蔽 - 转换类型或复用名称

Q: Swift 的 let 和 Rust 的 let 有什么区别?

A: 基本一致,都是默认不可变。主要区别在于:

  • Swift 的 let运行时常量(值在运行时确定)
  • Rust 的 const编译时常量(值在编译时确定)
  • Swift 没有 Rust 的 const 等价物

Q: 为什么 Swift 不像 Python 那样总是可变?

A: Swift 借鉴了函数式编程的理念:

  • 安全性:防止意外的数据修改
  • 并发安全:不可变数据可以在线程间安全共享
  • 编译器优化:不可变值让编译器能做更多优化

小结

核心要点

  1. let 声明不可变常量 - 这是 Swift 的默认设置
  2. var 声明可变变量 - 只在需要修改时使用
  3. Swift 自动推断类型 - let x = 5 推断为 Int
  4. 字符串插值用 \(变量) - 在字符串中嵌入表达式
  5. 变量遮蔽允许复用名称 - 在不同作用域可以重新声明

关键术语

  • Constant: 常量 (let 声明)
  • Variable: 变量 (var 声明)
  • Type Inference: 类型推断(编译器自动判断类型)
  • String Interpolation: 字符串插值(\(...) 语法)
  • Shadowing: 遮蔽(嵌套作用域重新声明同名变量)

术语表

English中文
Constant常量
Variable变量
Immutable不可变
Mutable可变
Type Inference类型推断
String Interpolation字符串插值
Shadowing遮蔽

完整示例:Sources/BasicSample/ExpressionSample.swift


知识检查

问题 1 🟢 (基础概念)

let x = 10
let y = x * 2
print(y)

A) 编译错误
B) 10
C) 20
D) 运行时错误

答案与解析

答案: C) 20

解析: x=10, y=10*2=20。Int 类型可以直接运算。

问题 2 🟡 (最佳实践)

以下哪种写法更符合 Swift 风格?

// A
var name = "Alice"
// name 从不被修改

// B
let name = "Alice"
答案与解析

答案: B) let name = "Alice"

解析: 如果变量从不被修改,使用 let 而不是 var。这表达了你的意图并让编译器能做优化。

原则:

默认用 let,需要修改时才改用 var

问题 3 🟡 (类型推断)

let pi = 3.14159
let radius = 5
let area = pi * Double(radius)

area 的类型是什么?

答案与解析

答案: Double

解析: pi 推断为 DoubleDouble(radius) 将 Int 转为 Double,Double * Double = Double。


延伸阅读

学习完变量与表达式后,你可能还想了解:

选择建议:

💡 记住:不可变性是 Swift 的默认设置 - 如果你不特别告诉它"这个要改变",Swift 会让它保持不变。这是为了你的安全!


继续学习

基础数据类型

开篇故事

去超市购物时,你会用不同的容器装东西:塑料袋装水果,玻璃罐装酱料,纸盒装鸡蛋。每种容器装不同类型、不同数量的东西。Swift 的数据类型也是这个道理。

IntDouble 像标了容量的瓶子,String 像可以无限拉长的绳子,Array 是排队的一列物品,Set 是一袋不重复的糖果,Dictionary 是你手机通讯录里的名字和号码对应表。理解了这些容器,你就能在程序中存储和操作任何数据。


本章适合谁

你已经学完变量与表达式,知道 letvar 的区别。这一章带你深入 Swift 的类型系统。如果你从未接触过强类型语言,或者好奇 Swift 和 Python/Rust 在类型方面的差异,这一章很适合你。


你会学到什么

完成本章后,你可以:

  1. 区分和使用 Swift 的数值类型(Int、Float、Double)和布尔类型(Bool)
  2. 创建和操作 String、Array、Set、Dictionary 四种集合类型
  3. 使用元组 (Tuple) 组合多个返回值
  4. 理解类型推断与显式类型标注的区别和适用场景
  5. 使用可选类型 (Optional) 安全地处理缺失值

前置要求

完成 变量与表达式 的学习。本章会大量使用 letvar 和字符串插值。


第一个例子

打开 Sources/BasicSample/DatatypeSample.swift,找到 collectionSample() 函数里的这段代码:

let array = [1, 2, 3, 4, 5]
var sum = Int32()
for i in array {
    sum = sum + Int32(i)
}
print("sum: \(sum)")

发生了什么?

  • [1, 2, 3, 4, 5] 创建了一个包含 5 个整数的数组,类型推断为 [Int]
  • Int32() 创建了初值为 0 的 32 位整数
  • Int32(i) 把数组中的每个 Int 转换为 Int32 再累加

输出:

sum: 15

原理解析

1. 数值类型家族

Swift 提供了完整的数值类型,每种类型有明确的位宽:

// 有符号整数
let age: Int = 30          // 平台自然位宽 (32位或64位)
let small: Int8 = -128     // 8位,范围 -128...127
let precise: Int32 = 100   // 32位

// 无符号整数
let count: UInt = 1000     // 无符号平台自然位宽
let byte: UInt8 = 255      // 8位,范围 0...255

// 浮点数
let pi: Float = 3.14           // 32位,约7位有效数字
let e: Double = 2.718281828    // 64位(Swift 默认浮点类型)

// 布尔值
let isSwiftFun: Bool = true
let isBoring: Bool = false

核心规则:不同类型之间不能直接运算。你需要显式转换:

let x: Int = 5
let y: Double = 3.14
// let z = x + y  // ❌ 错误!Int 和 Double 不能相加
let z = Double(x) + y  // ✅ 正确,z 的类型是 Double

2. String:字符串操作

字符串在 Swift 中是值类型,行为可预期:

let greeting = "Hello"
let name = "Swift"
let full = greeting + ", " + name + "!"  // 拼接

let age = 25
let info = "\(name) is \(age) years old"  // 插值

print("Length: \(full.count)")   // 字符数量
print("Is empty: \(full.isEmpty)")  // false
print("Contains Hello: \(full.contains("Hello"))")  // true

常见操作:

  • + 拼接两个字符串
  • \(...) 字符串插值,可以嵌入变量和表达式
  • .count 字符数量
  • .isEmpty 检查是否为空
  • .contains() 子串查找

3. Array:有序集合

数组保存一组相同类型的值,顺序固定:

// 创建
var shoppingList: [String] = ["Eggs", "Milk"]  // 显式类型
let numbers = [1, 2, 3, 4, 5]                  // 类型推断

// 初始化空数组
var emptyArray = [Int]()
var repeating = Array(repeating: 0.0, count: 3)  // [0.0, 0.0, 0.0]

// 添加元素
shoppingList.append("Flour")
shoppingList += ["Butter", "Sugar"]

// 访问
let first = shoppingList[0]           // "Eggs"
let count = shoppingList.count        // 5
let isEmpty = shoppingList.isEmpty    // false

// 遍历(带索引)
for (index, value) in shoppingList.enumerated() {
    print("Item \(index + 1): \(value)")
}

4. Set:无序不重复集合

Set 中的元素不重复,且无序。适合做去重和集合运算:

// 创建
var letters: Set<Character> = []
letters.insert("a")

var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]

// 集合运算
let oddDigits: Set = [1, 3, 5, 7, 9]
let evenDigits: Set = [0, 2, 4, 6, 8]

let allDigits = oddDigits.union(evenDigits)              // 并集: 0-9
let common = oddDigits.intersection(evenDigits)          // 交集: 空
let onlyOdd = oddDigits.subtracting([1, 3])              // 差集: [5, 7, 9]
let uniqueToBoth = oddDigits.symmetricDifference([2, 3, 5, 7])  // 对称差

// 检查和删除
if let removed = favoriteGenres.remove("Rock") {
    print("Removed: \(removed)")
}

5. Dictionary:键值对映射

Dictionary 通过唯一的键快速查找对应的值:

// 创建
var namesOfIntegers: [Int: String] = [:]
namesOfIntegers[16] = "sixteen"

var airports: [String: String] = [
    "YYZ": "Toronto Pearson",
    "DUB": "Dublin"
]

// 添加和修改
airports["LHR"] = "London"      // 新增
airports["YYZ"] = "Toronto"     // 修改已有键

// 访问(返回可选类型!)
let code = airports["LHR"]      // Optional("London")
let missing = airports["XXX"]   // nil

// 遍历
for (airportCode, airportName) in airports {
    print("\(airportCode): \(airportName)")
}

// 只遍历键或值
for code in airports.keys { print(code) }
for name in airports.values { print(name) }

重要:通过键访问字典时返回的是可选类型。键不存在时返回 nil,不会崩溃。

6. 元组 (Tuple)

元组把多个值组合成一个复合值:

// 无名元组
let http404 = (404, "Not Found")
print("Status: \(http404.0), Message: \(http404.1)")

// 命名元组(推荐!可读性更好)
let result = (statusCode: 200, description: "OK")
print("Status: \(result.statusCode)")

// 函数返回多个值(来自 DatatypeSample)
func minMax(array: [Int]) -> (min: Int, max: Int) {
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array[1..<array.count] {
        if value < currentMin { currentMin = value }
        else if value > currentMax { currentMax = value }
    }
    return (currentMin, currentMax)
}

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("min is \(bounds.min), max is \(bounds.max)")
// 输出: min is -6, max is 109

7. 类型推断 vs 显式标注

Swift 编译器能自动推断大多数类型,但你也可以显式指定:

// 类型推断(编译器自动判断)
let name = "Swift"      // 推断为 String
let count = 42          // 推断为 Int
let pi = 3.14           // 推断为 Double

// 显式类型标注
let name2: String = "Swift"
let count2: Int = 42
let pi2: Double = 3.14
let smallNum: Int8 = 127

何时需要显式标注?

  • 编译器无法推断(如空集合 [Int]()
  • 你想覆盖默认推断(如 Float 而不是 Double
  • 代码意图需要更清晰时

8. 可选类型 (Optional)

Swift 用可选类型安全地表示"值可能存在也可能不存在":

// 声明
let maybeName: String? = "Alice"    // 有值
let nothing: String? = nil           // 无值

// 可选绑定(安全解包)
if let name = maybeName {
    print("Hello, \(name)!")   // 只在有值时执行
} else {
    print("No name provided")
}

// 处理 nil 的情况
if let name = nothing {
    print("Hello, \(name)!")
} else {
    print("No name provided")   // 会执行这个分支
}

为什么需要可选类型? 在 Python 中,缺失值是 None,访问一个不存在的键会抛异常。Swift 把"可能为空"这件事变成了类型系统的一部分。函数返回 String? 时,你必须处理 nil 的情况,这从编译阶段就消除了大量空指针错误。


常见错误

错误 1: 不同类型之间直接运算

let x: Int = 5
let y: Double = 3.0
let z = x + y  // ❌

编译器输出:

error: binary operator '+' cannot be applied to values of type 'Int' and 'Double'

修复方法:

let z = Double(x) + y  // ✅ 先把 Int 转为 Double

错误 2: 数组越界访问

let fruits = ["Apple", "Banana"]
print(fruits[5])  // ❌ 运行时崩溃!

编译器输出:

Fatal error: Index out of range

修复方法:

if fruits.count > 5 {
    print(fruits[5])
} else {
    print("Index out of bounds")
}

错误 3: 强制解包 nil 可选值

let dictionary = ["name": "Alice"]
let age = dictionary["age"]!  // ❌ 键 "age" 不存在

编译器输出:

Fatal error: unexpectedly found nil while unwrapping an optional value

修复方法:

if let age = dictionary["age"] {
    print("Age: \(age)")
} else {
    print("Age not found")
}

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
整数类型int (任意精度)i32, i64Int, Int8, Int32Swift 的 Int 是平台自适应位宽
浮点类型float (64位)f32, f64Float, DoubleSwift 默认推 Double
布尔类型True/Falsetrue/falsetrue/falsePython 首字母大写
字符串"hello""hello".to_string()"hello"Python 字符串可变;Swift/Rust 默认不可变
数组/列表list = [1, 2]vec![1, 2][1, 2, 3]Swift 数组同类型;Python 列表可混
集合set = {1, 2}HashSet::new()Set([1, 2])Swift 用字面量推导集合类型
字典/哈希表dict = {"a": 1}HashMap::new()["a": 1]三种语言的字典语法最接近
元组(1, "a")(1, "a".to_string())(1, "a")Python 和 Swift 元组语法几乎相同
可选类型NoneOption<T>T?Swift 用 ? 后缀最简洁
类型推断动态类型let x = 5let x = 5Python 完全动态;Rust/Swift 静态推断

动手练习

练习 1: 类型判断

不运行代码,判断下面每个变量的类型:

let a = 42
let b = 3.14
let c = [1, 2, 3]
let d: Float = 2.5
let e = "hello"
点击查看答案
  • a: Int
  • b: Double
  • c: [Int]
  • d: Float(显式标注)
  • e: String

练习 2: 字典操作

创建一个字典存储你的三门课成绩,然后计算平均分:

// 你的代码
点击查看参考实现
let scores = ["Math": 95, "English": 88, "Science": 92]
var total = 0
for score in scores.values {
    total += score
}
let average = Double(total) / Double(scores.count)
print("Average score: \(average)")
// 输出: Average score: 91.666...

注意:Double(total) / Double(scores.count) 中的转换是必须的,因为 IntInt 相除还是 Int,小数部分会被截断。

练习 3: 可选绑定

给一个可能为 nil 的字典值做安全访问:

let config: [String: String]? = nil
// 安全地读取 "theme" 键

let config2: [String: String]? = ["theme": "dark"]
// 安全地读取 "theme" 键
点击查看参考实现
// 第一层:可选字典
if let config = config {
    // 第二层:可选值
    if let theme = config["theme"] {
        print("Theme: \(theme)")
    }
} else {
    print("Config is nil")  // 会执行这句
}

// 更简洁的写法:if let 链
if let config2 = config2, let theme = config2["theme"] {
    print("Theme: \(theme)")  // Theme: dark
}

故障排查 FAQ

Q: IntInt32 有什么区别?什么时候该用哪个?

A: Int 是当前平台的自然位宽,在 64 位机器上是 64 位,在 32 位机器上是 32 位。Int32 固定 32 位。

  • 日常编程用 Int:这是 Swift 的默认选择,性能最好
  • 和 C/外部库交互时用固定位宽类型:如 Int32UInt8,确保二进制布局匹配
  • 处理网络协议或文件格式时用固定位宽类型:确保跨平台一致性

Q: 为什么字典访问返回值要加 ?

A: 字典用键查询值时,键可能不存在。返回 T?(可选类型)让编译器强制你处理"找不到"的情况。

let dict = ["a": 1]
let value: Int? = dict["b"]   // nil
let forced = dict["b"]!       // 💥 崩溃!不要这样做

Python 用 dict.get("b") 返回 None,行为类似。但 Python 不会阻止你对 None 调用方法,Swift 会在编译阶段就拦住你。

Q: 元组和结构体 (struct) 有什么区别?

A: 元组是临时的轻量组合,适合短场景返回值(如函数返回多个值)。结构体是正式类型,定义了字段名和方法,适合在代码中反复使用。

  • 临时分组用 元组
  • 需要复用、传递、扩展用 结构体

小结

核心要点:

  1. 数值类型有明确位宽IntInt8FloatDouble,不同类型不能直接运算
  2. String 是值类型 — 支持拼接 (+) 和插值 (\(...)
  3. Array 有序可重复,Set 无序不重复,Dictionary 键值映射 — 三种各有适用场景
  4. 元组组合多值 — 命名元组比 tuple.0 可读性好得多
  5. 可选类型 T? 安全处理缺失值 — 用 if let 绑定解包,永远不要 ! 强制解包

关键术语:

  • Type Inference: 类型推断(编译器自动判断)
  • Optional: 可选类型(值可能为 nil)
  • Optional Binding: 可选绑定(if let 安全解包)
  • Tuple: 元组(复合值类型)
  • Explicit Type Annotation: 显式类型标注

术语表

English中文
Integer整数
Float单精度浮点数
Double双精度浮点数
Boolean布尔值
String字符串
Array数组
Set集合
Dictionary字典
Tuple元组
Optional可选类型
Optional Binding可选绑定
Type Inference类型推断
Type Annotation类型标注
Collection集合类型

完整示例:Sources/BasicSample/DatatypeSample.swift


知识检查

问题 1 🟢 (基础概念)

let numbers = [1, 2, 3]
let moreNumbers = numbers + [4, 5]

moreNumbers 的值和类型是什么?

答案与解析

答案: [1, 2, 3, 4, 5],类型 [Int]

解析: 数组用 + 拼接,结果是新数组。原有 numbers 不变(因为用 let 声明)。

问题 2 🟡 (可选类型)

let dict = ["key": "value"]
print(dict["missing"] ?? "default")

输出是什么?

答案与解析

答案: default

解析: dict["missing"] 返回 nil?? 空值合并运算符在可选值为 nil 时提供默认值。等价于:

let result = dict["missing"] != nil ? dict["missing"]! : "default"

问题 3 🔴 (类型推断 + 转换)

let a = 5
let b = 3.0
let c = Double(a) + b
let d = a + Int(b)

cd 的值和类型分别是什么?

答案与解析

答案: c = 8.0Double),d = 8Int

解析:

  • Double(a)5 转为 5.05.0 + 3.0 = 8.0(Double)
  • Int(b)3.0 转为 35 + 3 = 8(Int)
  • 关键是看运算中最高精度的类型来决定结果类型。

延伸阅读

学完基础数据类型后,你可能还想了解:

选择建议:

  • 想深入理解循环和条件判断 → 继续学习 控制流
  • 已有编程经验 → 跳到 函数

💡 记住:Swift 是强类型语言。编译器比你更早知道类型错误,善用类型推断,但不要害怕显式标注。


继续学习

  • 下一步:控制流 - 条件判断和循环
  • 相关:函数 - 学习如何组织代码
  • 进阶:枚举 - Swift 最强大的类型之一

控制流

开篇故事

你每天早上醒来,大脑就在运行控制流。如果室外温度低于 10 度,你穿外套。如果今天是星期一到星期五,你去上班。否则,你休息。你会反复做同一件事直到满足某个条件,比如刷牙刷了两分钟才停下。

程序也是如此。代码不一定从上到下逐行执行。有时你要根据条件走不同的分支,有时你要反复做一件事直到某个条件满足,有时你要提前结束一段逻辑。Swift 提供了丰富的控制流工具来描述所有这些场景。


本章适合谁

你已经学完变量与数据类型,理解了 letvar 和各种集合类型。这一章带你学会让程序做出决策和重复执行任务。如果你写过任何语言的条件判断或循环,你会在这里看到 Swift 的独到设计。


你会学到什么

完成本章后,你可以:

  1. 使用 if/else 进行条件分支判断
  2. 使用 switch/case 处理多路分支,包括范围匹配和元组匹配
  3. 使用 for-in 遍历数组、字典和各种范围
  4. 使用 whilerepeat-while 编写循环
  5. 使用 guard 做前置条件检查和早期退出

前置要求

完成 基础数据类型 的学习。本章会大量使用 Bool、比较运算符和 String


第一个例子

打开 Sources/BasicSample/ControlFlowSample.swift,找到 controlFlowConditionSample() 函数里的这段代码:

let temperature = 25
if temperature < 0 {
    print("Freezing!")
} else if temperature < 20 {
    print("Chilly")
} else {
    print("Comfortable")
}

发生了什么?

  • temperature < 0 是一个布尔表达式,结果为 truefalse
  • 三个分支互斥,只会执行其中一个
  • 25 不小于 0 也不小于 20,所以走 else 分支

输出:

Comfortable

原理解析

1. if/else 条件分支

Swift 的 if 要求条件必须是布尔值,不能像 C/Python 那样用非零整数替代 true

let score = 85

if score >= 90 {
    print("A")
} else if score >= 80 {
    print("B")
} else if score >= 70 {
    print("C")
} else {
    print("F")
}

Swift 的一个细节if 后面不需要括号,但大括号 {} 是必须的。这是强制的,不能省略。

三元条件运算符也是可选的写法:

let status = score >= 60 ? "Pass" : "Fail"

2. switch/case 多路分支

Swift 的 switch 比 C 语言强大得多:不需要写 break,必须是穷尽的(覆盖所有情况),而且可以匹配各种模式。

let fruit = "apple"
switch fruit {
case "apple":
    print("🍎 Apple")
case "banana":
    print("🍌 Banana")
case "cherry":
    print("🍒 Cherry")
default:
    print("Unknown fruit")
}

关键差异

  • 不需要 break:Swift 的 case 执行完后自动退出,不会"穿透"到下一个 case。如果你确实需要穿透,用 fallthrough
  • 必须穷尽:每个可能的值都要被覆盖。字符串有无限可能,所以必须有 default。如果枚举只有三个 case,你写了三个 case 就够了,不需要 default

3. switch 模式匹配

Swift 的 switch 可以匹配范围、元组等各种模式:

// 范围匹配
let year = 2024
switch year {
case 2000..<2010:
    print("2000s")
case 2010..<2020:
    print("2010s")
case 2020...2100:
    print("2020s or later")
default:
    print("Ancient times")
}

// 元组匹配
let point = (3, 0)
switch point {
case (0, 0):
    print("Origin")
case (_, 0):
    print("On x-axis")
case (0, _):
    print("On y-axis")
case (-2...2, -2...2):
    print("Inside the box")
default:
    print("Outside")
}
// 输出: On x-axis (_ 是通配符,匹配任意值)

4. for-in 遍历范围

Swift 提供两种范围运算符:

// 闭区间 ... (包含结束值)
print("1...3:")
for i in 1...3 { print(i) }
// 输出: 1, 2, 3

// 半开区间 ..< (不包含结束值)
print("1..<3:")
for i in 1..<3 { print(i) }
// 输出: 1, 2

// stride 步进迭代
print("Stride 0 to 10 by 3:")
for i in stride(from: 0, to: 10, by: 3) {
    print(i)
}
// 输出: 0, 3, 6, 9

to vs throughstride(from:to:by:) 是半开区间(不包含 to 的值);如果你想要闭区间,使用 stride(from:through:by:)

5. for-in 遍历数组和字典

// 遍历数组
let names = ["Alice", "Bob", "Charlie"]
for name in names {
    print("Hello, \(name)!")
}

// 带索引遍历
for (index, name) in names.enumerated() {
    print("Person \(index + 1): \(name)")
}

// 遍历字典
let scores = ["Math": 95, "English": 88]
for (subject, score) in scores {
    print("\(subject): \(score)")
}

// 只遍历键或值
for subject in scores.keys { print(subject) }
for score in scores.values { print(score) }

6. while 和 repeat-while

while 先检查条件,repeat-while 先执行至少一次再检查:

// while
var counter = 0
while counter < 3 {
    print("Counter: \(counter)")
    counter += 1
}
// 输出: 0, 1, 2

// repeat-while
var total = 0
repeat {
    total += 1
} while total < 3
print("Total: \(total)")
// 输出: 3

区别repeat-while 保证循环体至少执行一次。C 语言里的 do-while 在 Swift 里叫 repeat-while

7. guard:早期退出

guard 是一种"守卫"语句,当条件不满足时提前退出当前作用域:

func greet(_ name: String?) {
    guard let name = name else {
        print("No name provided")
        return
    }
    // name 在这里已经是 unwrapped 的 String
    print("Hello, \(name)!")
}

greet("Alice")     // Hello, Alice!
greet(nil)         // No name provided

guard 的核心思想:"我不关心正确的路径,我只关心出错时该怎么办"。这比嵌套 if let 清晰得多:

// 不推荐:嵌套 if let
func greetBad(_ name: String?) {
    if let name = name {
        print("Hello, \(name)!")
    } else {
        print("No name provided")
    }
}

// 推荐:guard 提前退出
func greetGood(_ name: String?) {
    guard let name = name else {
        print("No name provided")
        return
    }
    print("Hello, \(name)!")
}

8. 标签语句 (Labeled Statements)

Swift 支持给循环打标签,这样可以在嵌套循环中精确控制 breakcontinue 跳出的层级:

outerLoop: for i in 1...3 {
    for j in 1...3 {
        if j == 2 { break outerLoop }  // 跳出外层循环
        print("(\(i), \(j))")
    }
}
// 输出: (1, 1)
// j == 2 时直接跳出 outerLoop,不会继续 i=2 或 i=3 的迭代

这个特性在 Python 中没有对应物。Python 你需要设置标志变量或者用函数提前 return 才能跳出多层循环。


常见错误

错误 1: switch 没有覆盖所有情况

let number = 5
switch number {
case 1:
    print("one")
case 2:
    print("two")
// 缺少 default 或其他 case 覆盖 3, 4, 5, ...
}

编译器输出:

error: switch must be exhaustive

修复方法:

switch number {
case 1:
    print("one")
case 2:
    print("two")
default:
    print("other")  // ✅ 覆盖其余所有情况
}

错误 2: if 条件不是布尔值

let count = 5
if count {  // ❌ 错误!
    print("has items")
}

编译器输出:

error: condition for 'if' must be of type 'Bool'

修复方法:

if count > 0 {  // ✅ 显式布尔表达式
    print("has items")
}

错误 3: switch case 中缺少可执行语句

let x = 3
switch x {
case 3:  // ❌ 空 case
case 4:
    print("three or four")
}

编译器输出:

error: case label in a non-exhaustive switch does not cover all possible values

修复方法: Swift 不允许空 case(除非用 fallthrough@unknown default):

switch x {
case 3:
    fallthrough  // ✅ 显式声明穿透到下一个 case
case 4:
    print("three or four")
default:
    break  // 也可以什么都不做
}

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
if/elseif x > 0:if x > 0 { }if x > 0 { }Python 用缩进;Rust/Swift 用大括号
条件类型任意 Truthy 值boolBoolSwift 严格要求 Bool,不接受整数
switch无(用 if/elif)matchswitchPython 3.10 有 match,但能力有限
break 关键字需要不需要不需要Swift/Rust 的 case 不穿透
范围遍历range(1, 4)1..=31..31...31..<3三种语言范围语法各不相同
遍历数组for x in lst:for x in &vecfor x in array语法几乎一致
带索引遍历enumerate(lst)iter.enumerate()array.enumerated()Swift 返回 (index, element) 元组
while 循环while cond:while cond { }while cond { }语义完全一致
do-whileloop { }repeat { } whilePython 没有 do-while
guard / 早期退出?if letguardSwift 独有 guard 语句
跳出外层循环无(需标志变量)标签循环 outer: loop标签循环 outerLoop:Swift/Rust 都支持标签

动手练习

练习 1: 预测输出

for i in 1...5 {
    if i % 2 == 0 {
        continue
    }
    print(i)
}

输出什么?

点击查看答案

输出:

1
3
5

解析: i % 2 == 0 时遇到偶数,continue 跳过本次循环,不执行后面的 print。奇数时正常输出。

练习 2: FizzBuzz

打印 1 到 15,但如果是 3 的倍数打印 "Fizz",5 的倍数打印 "Buzz",同时是 3 和 5 的倍数打印 "FizzBuzz",其他打印数字本身。

点击查看参考实现
for i in 1...15 {
    if i % 15 == 0 {
        print("FizzBuzz")
    } else if i % 3 == 0 {
        print("Fizz")
    } else if i % 5 == 0 {
        print("Buzz")
    } else {
        print(i)
    }
}

注意:i % 15 == 0 的检查必须放在最前面,否则会被单独的 3 或 5 的分支截获。

练习 3: guard 提前退出

写一个函数 calculateBMI(weight:height:),要求 weight 和 height 都为正数。如果任一参数非正,用 guard 提前返回 nil

点击查看参考实现
func calculateBMI(weight: Double, height: Double) -> Double? {
    guard weight > 0 else {
        print("Weight must be positive")
        return nil
    }
    guard height > 0 else {
        print("Height must be positive")
        return nil
    }
    return weight / (height * height)
}

print(calculateBMI(weight: 70, height: 1.75) ?? "N/A")  // 22.857...
print(calculateBMI(weight: -5, height: 1.75) ?? "N/A")   // nil

故障排查 FAQ

Q: switch 为什么必须有 default?

A: 因为 Swift 是强类型语言,编译器需要确保每个可能的值都有对应的分支。字符串类型有无限个可能的值,如果你只写了几个具体的 case,编译器就无法确定没写到的值该怎么办。

对于枚举类型,如果写了所有 case,就不需要 default。编译器知道枚举的所有可能值。

Q: for-in 循环中能不能修改集合?

A: 不能直接修改正在遍历的集合。Swift 在遍历时会锁定集合状态。如果你想修改,需要遍历一个副本或者收集要修改的索引再操作:

var numbers = [1, 2, 3, 4, 5]
// ❌ 错误
// for n in numbers {
//     numbers.append(n * 2)  // 运行时错误
// }

// ✅ 正确:遍历副本
for n in numbers {
    numbers.append(n * 2)
}

Q: guard 和 if let 什么时候用哪个?

A:

  • guard: 用于"前置条件不满足就退出"的场景。它让 happy path(正常执行路径)保持浅层缩进。适合函数开头的参数校验。
  • if let: 用于"值存在时才执行某段逻辑"的场景。happy path 在 if 的大括号内。适合中间流程的条件判断。

选择的标准在于:你是想在"出错时退出"(guard)还是"成功时进入"(if let)。


小结

核心要点:

  1. if/else 做条件分支 — 条件必须是 Bool,大括号不可省
  2. switch 自动 break,必须穷尽 — 支持范围、元组等模式匹配
  3. for-in 遍历数组、字典、范围enumerate() 带索引
  4. while 先检查,repeat-while 先执行 — 后者至少执行一次
  5. guard 提前退出 — 参数校验利器,保持代码扁平

关键术语:

  • Conditional Branching: 条件分支(if/else)
  • Pattern Matching: 模式匹配(switch case)
  • Range: 范围(.....<
  • Early Exit: 早期退出(guard)
  • Labeled Statement: 标签语句(name: for

术语表

English中文
Control Flow控制流
Conditional Statement条件语句
Branch分支
Loop循环
Switch Case开关语句
Pattern Matching模式匹配
Range范围
Labeled Statement标签语句
Guard守卫语句
Early Exit早期退出
Exhaustive穷尽的
Fallthrough穿透

完整示例:Sources/BasicSample/ControlFlowSample.swift


知识检查

问题 1 🟢 (基础概念)

for i in 1..<4 {
    print(i)
}

输出什么?

答案与解析

答案:

1
2
3

解析: 1..<4 是半开区间,包含 1 但不包含 4。所以遍历 1, 2, 3。

问题 2 🟡 (switch 模式匹配)

let point = (1, 0)
switch point {
case (0, 0):
    print("Origin")
case (_, 0):
    print("On x-axis")
case (0, _):
    print("On y-axis")
default:
    print("Other")
}

输出什么?

答案与解析

答案: On x-axis

解析: point(1, 0)。第一个 case (0, 0) 不匹配。第二个 case (_, 0) 匹配 —— 第一个位置是通配符任意值都行,第二个位置是 0。case 从上到下匹配,第一个命中就不再尝试后面的。

问题 3 🔴 (guard + 嵌套循环)

func process(data: [Int]?) {
    guard let data = data else {
        print("No data")
        return
    }
    searchLoop: for item in data {
        for divisor in 2...item {
            if item % divisor == 0 && divisor != item {
                print("\(item) is not prime")
                break searchLoop
            }
        }
        print("\(item) is prime")
    }
}
process(data: [7, 8])

输出什么?

答案与解析

答案: 7 is prime

解析:

  • data[7, 8],guard 通过
  • 遍历 item = 7:内层循环 divisor 从 2 到 6,没有任何数能整除 7,所以打印 7 is prime
  • 遍历 item = 8:内层循环 divisor = 2 时,8 % 2 == 02 != 8,进入 if 分支。打印 8 is not prime,然后 break searchLoop 跳出整个外层循环
  • 最终结果:7 is prime,然后 8 is not prime,循环结束

延伸阅读

学完控制流后,你可能还想了解:

选择建议:

  • 刚学完条件循环 → 继续学习 函数 - 把逻辑组织成可复用单元
  • 已有其他语言经验 → 跳到 枚举 - Swift 枚举是你需要重点关注的内容

💡 记住:Swift 的控制流设计哲学是"让正确的写法自然,让错误的写法不可能"。强制 Bool 条件、必须穷尽的 switch、自动 break 的 case —— 这些设计减少了你能犯的错误。


继续学习

  • 下一步:函数 - 学习如何组织代码
  • 相关:枚举 - Swift 最强大的类型之一
  • 进阶:错误处理 - 用 guard 处理异常场景

函数

开篇故事

想象你在一家餐厅工作。顾客点菜后,厨房里的厨师按照固定配方烹饪,然后把完整的菜品端上桌。每个配方就是一个函数:它接收食材(输入),经过一系列步骤加工,最后产出菜品(输出)。

在编程中,函数让你把重复的逻辑封装成可复用的"配方"。当你需要同一道菜时,不需要重新发明配方,只需要调用它即可。Swift 的函数设计兼具灵活性和安全性,让你可以自由组合参数与返回值。


本章适合谁

如果你希望学会如何组织代码、减少重复、写出可读性强的程序,本章适合你。无论你是第一次写函数,还是从其他语言转来学习 Swift 的函数特性,本章都会帮你快速上手。


你会学到什么

完成本章后,你可以:

  1. 使用 func 关键字定义和调用函数
  2. 理解外部参数名(external name)和省略符 _ 的使用
  3. 为参数设置默认值,声明可变参数列表
  4. 使用 inout 参数和元组实现多返回值
  5. 把函数当作值传递,包括嵌套函数和闭包返回

前置要求

本章前置知识:阅读过 变量与表达式,熟悉 letvar 和基本类型。


第一个例子

打开 Sources/BasicSample/FunctionSample.swift,找到最简单的函数定义:

func greet(person: String, from location: String) -> String {
    "Hello \(person)! I'm from \(location)."
}

print(greet(person: "Alice", from: "Beijing"))
// 输出: Hello Alice! I'm from Beijing.

发生了什么?

  • func greet(person: String, from location: String) — 定义函数,参数名前面带有一个标签
  • -> String — 指定返回类型为 String
  • 最后一行隐式返回结果(省略了 return 关键字)

原理解析

1. 参数标签与外部名称

Swift 函数参数的默认行为是每个参数都有一个外部名称(标签),调用时必须使用:

func greet(person: String, from location: String) -> String {
    "Hello \(person) from \(location)"
}

// 调用时必须带上标签
greet(person: "Alice", from: "Beijing")

这让调用代码像句子一样可读。如果你希望调用时不需要标签,用 _ 来省略:

func sum(_ a: Int, _ b: Int) -> Int {
    a + b
}

sum(3, 5)  // 不需要标签

2. 默认参数值

Swift 允许为参数设置默认值,调用时可以直接省略:

func greet(person: String, greeting: String = "Hello") -> String {
    "\(greeting), \(person)!"
}

print(greet(person: "Bob"))                      // Hello, Bob!
print(greet(person: "Charlie", greeting: "Hi"))  // Hi, Charlie!

注意:默认值让函数调用更灵活,但带默认值的参数通常放在参数列表末尾。

3. 可变参数(Variadic Parameters)

当参数数量不确定时,使用 ... 来声明可变参数:

func arithmeticMean(_ numbers: Double...) -> Double {
    let total = numbers.reduce(0, +)
    return total / Double(numbers.count)
}

arithmeticMean(1, 2, 3, 4, 5)      // 3.0
arithmeticMean(3.0, 8.25, 18.75)   // 10.0

函数内部,numbers 是一个 Array<Double>,你可以对它做任何数组操作。

4. In-out 参数

默认情况下,函数参数是值传递(只读副本)。如果你需要在函数内部修改传入的变量,使用 inout

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}

var x = 3
var y = 107
swapTwoInts(&x, &y)
print("After: x=\(x), y=\(y)")  // x=107, y=3

关键点:

  • 函数定义里参数标记 inout
  • 调用时变量前面加 &,表明地址传递
  • 传入的必须是有实际内存位置的 var,不能是 let

5. 多返回值:元组

Swift 允许函数返回多个值,使用元组(tuple):

func minMax(array: [Int]) -> (min: Int, max: Int)? {
    guard !array.isEmpty else { return nil }
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array.dropFirst() {
        if value < currentMin { currentMin = value }
        else if value > currentMax { currentMax = value }
    }
    return (currentMin, currentMax)
}

if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
    print("Min: \(bounds.min), Max: \(bounds.max)")
}
// 输出: Min: -6, Max: 109

返回类型 (min: Int, max: Int)? 中,? 表示可选的元组(可能返回 nil)。通过 bounds.minbounds.max 可以按名称访问元素。

6. 函数作为值和嵌套函数

Swift 中函数也是值,可以赋值给变量、作为返回值:

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 10)
print(incrementByTen())  // 10
print(incrementByTen())  // 20
print(incrementByTen())  // 30

makeIncrementer 返回一个内部定义的函数,每次调用都会累加。这是闭包(closure)的基础概念,在后续章节还会继续深入。


常见错误

错误 1: 忘记参数标签

func greet(person: String, from location: String) -> String {
    "Hi \(person) from \(location)"
}

greet("Alice", "Beijing")  // ❌

编译器输出:

error: missing argument labels 'person:from:' in call

修复方法:

greet(person: "Alice", from: "Beijing")  // ✅ 补充标签

错误 2: inout 参数传入了不可变的 let

func swapTwoInts(_ a: inout Int, _ b: inout Int) { /* ... */ }

let x = 3, y = 5
swapTwoInts(&x, &y)  // ❌

编译器输出:

error: cannot pass immutable value as inout argument: 'x' is a 'let' constant

修复方法:

var x = 3, y = 5  // 改为 var
swapTwoInts(&x, &y)  // ✅

错误 3: 可变参数后面不能有其他参数

func bad(_ numbers: Double..., extra: String) -> Void { }  // ❌

编译器输出:

error: a parameter may not be marked inout or followed by a parameter that is marked inout, or marked with the ellipsis ('...')

修复方法:

func correct(extra: String, _ numbers: Double...) -> Void { }  // 把 ... 放最后

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
函数定义def name(x):fn name(x: Type) -> Retfunc name(x: Type) -> RetSwift/Rust 返回类型用箭头
参数标签无,位置参数func name(label param: Type)Swift 默认有外部标签
省略标签*argsN/A_ 前缀Swift 用 _ 隐藏外部名
默认参数def f(x=1):无(用 builder 模式)func f(x: Int = 1)Rust 不支持直接写默认值
可变参数*args通过宏 println!... 后缀Rust 使用不同的范式
In-out 参数无(只能返回新值)&mut 引用inout + & 调用Swift 的 inout 语义最明确
多返回值return (a, b)(元组)(a, b)(元组)(a: T, b: U)(命名元组)Swift 元组可以命名元素
函数作为值f = lambda x: x闭包 `xx`

动手练习

练习 1: 预测输出

不运行代码,预测下面代码的输出:

func multiply(_ a: Int, _ b: Int) -> Int {
    a * b
}

let result = multiply(7, 6)
print("Result: \(result)")
点击查看答案

输出:

Result: 42

解析: multiply(7, 6) 计算 7 乘 6,结果为 42。

练习 2: 写一个带默认参数的函数

写一个名为 describe 的函数,接收姓名(person)和爱好(hobby),其中 hobby 有默认值 "reading"。调用时只用传姓名。

点击查看参考实现
func describe(person: String, hobby: String = "reading") -> String {
    "\(person) enjoys \(hobby)."
}

print(describe(person: "Alice"))                     // Alice enjoys reading.
print(describe(person: "Bob", hobby: "swimming"))    // Bob enjoys swimming.

练习 3: In-out 实现翻倍函数

写一个 inout 函数 doubleInPlace(_ value: inout Int),把传入的整数值翻一倍。

点击查看参考实现
func doubleInPlace(_ value: inout Int) {
    value = value * 2
}

var number = 21
doubleInPlace(&number)
print(number)  // 42

故障排查 FAQ

Q: Swift 的函数参数为什么默认有外部标签?

A: 让调用代码像自然语言一样可读:

greet(person: "Alice", from: "Beijing")

这句话本身就像英文句子。如果你觉得干扰,可以用 _ 省略,但通常建议在语义不清晰时保留标签。

Q: inout 和直接返回新值有什么区别?

A: 语义层面,inout 表示"修改已有状态",适合交换、累加等场景。返回新值的方式更函数式,Swift 通常更推荐后者。两者性能差异在现代 Swift 编译器中几乎可以忽略。

Q: 函数可以返回多个值吗?

A: 可以,使用元组。元组不仅返回多个值,还可以给每个元素命名,调用后用 .元素名 访问,非常直观。


小结

核心要点:

  1. func 定义函数 — 参数带外部标签(或用 _ 省略),-> Type 指定返回类型
  2. 默认参数值 — 用 = defaultValue 提供可选参数
  3. 可变参数 ... — 接受任意数量同类型参数,内部以数组形式访问
  4. inout 参数 — 用 & 传入 var 变量,在函数内可以修改原始值
  5. 元组返回多值(min: Int, max: Int) 让返回结果自带名称

关键术语:

  • Function: 函数(用 func 定义的可复用代码块)
  • Parameter Label: 参数标签(调用时使用的外部名称)
  • Default Parameter Value: 默认参数值(调用时可选)
  • Variadic Parameter: 可变参数(... 语法)
  • In-out Parameter: 输入输出参数(允许修改传入变量)
  • Tuple: 元组(组合多个值的轻量结构)

术语表

English中文
Function函数
Parameter Label参数标签
External Name外部名称
Default Parameter Value默认参数值
Variadic Parameter可变参数
In-out Parameter输入输出参数
Tuple元组
Nested Function嵌套函数
Closure闭包
Return Type返回类型

完整示例:Sources/BasicSample/FunctionSample.swift


知识检查

问题 1 🟢(基础概念)

func add(_ a: Int, _ b: Int) -> Int { a + b }
let result = add(10, 32)

result 的值是多少?

A) "1032" B) 42 C) 编译报错 D) 运行时错误

答案与解析

答案: B) 42

解析: 两个 Int 相加,10 + 32 = 42。参数使用 _ 省略外部标签,直接传值即可。

问题 2 🟡(参数标签)

下面哪个调用是正确的?

func connect(to host: String, port: Int) { }

A) connect("localhost", 8080) B) connect(to: "localhost", port: 8080) C) connect(host: "localhost", port: 8080)

答案与解析

答案: B) connect(to: "localhost", port: 8080)

解析: Swift 中参数标签是 toport,调用时必须使用对应标签。注意参数名称和本地标签可以不同。

问题 3 🔴(进阶理解)

func makeCounter() -> () -> Int {
    var count = 0
    return { count += 1; return count }
}

let counter = makeCounter()
print(counter(), counter(), counter())

输出是什么?

答案与解析

答案: 1 2 3

解析: 函数返回一个闭包,闭包捕获了外部变量 count。每次调用闭包时,count 会累加。这体现了 Swift 闭包的捕获机制。


延伸阅读

学习完函数后,你可能还想了解:

选择建议:

  • 初学者 → 继续学习 枚举
  • 有经验的程序员 → 跳到 结构体

记住:Swift 的函数设计强调可读性。参数标签让你的调用像自然语言一样清晰,这是一项值得坚持的编码习惯。


继续学习

  • 下一步:枚举 — 用 enum 定义类型安全的选项
  • 相关:结构体 — 值类型数据模型
  • 进阶:协议 — 面向协议的编程范式

枚举

开篇故事

想象你是一个气象站的预报员。天气有几种固定的状态:晴天、雨天、雪天、多云。但你不能简单用一个字符串表示,因为类型拼写错误会让程序崩溃。

Swift 的枚举(enum)就是为这种场景而生的。它限制值只能从一组预定义的选项中选择,让编译器帮你检查所有可能性。不仅如此,Swift 的枚举比其他语言更强大:它可以携带额外数据,可以定义行为方法,甚至可以递归定义数据结构。


本章适合谁

如果你希望用类型安全的方式管理一组相关的选择,或者想理解 Swift 枚举为什么被称为"最像数据结构的枚举",本章适合你。从简单的状态标识到复杂的模式匹配,枚举是 Swift 编程中最重要的基础之一。


你会学到什么

完成本章后,你可以:

  1. 使用 enum 关键字定义基本枚举类型
  2. 理解关联值(associated values)和原始值(raw values)的区别
  3. 使用 CaseIterable 协议和 allCases 遍历所有枚举值
  4. switch 语句对枚举进行穷尽匹配
  5. 使用 indirect 关键字定义递归枚举

前置要求

本章前置知识:阅读过 函数,熟悉函数基本定义和 switch 语句的概念。


第一个例子

打开 Sources/BasicSample/EnumSample.swift,找到最简单的枚举定义:

enum CompassPoint {
    case north, south, east, west
}

var direction = CompassPoint.north
direction = .south  // 类型已知时可省略前缀
print("Direction: \(direction)")
// 输出: Direction: south

发生了什么?

  • enum CompassPoint — 定义一个枚举类型
  • case north, south, east, west — 列出所有可能的值
  • CompassPoint.north — 通过点语法访问枚举值,类似命名空间

原理解析

1. 基本枚举定义

Swift 枚举的每个值称为成员(member)。枚举成员不属于整数或字符串,它们是独立的类型:

enum Planet {
    case mercury, venus, earth, mars, jupiter
}

let homePlanet = Planet.earth

与 C 语言不同,Swift 的枚举值没有隐式的整数编号。每个值就是一个纯粹的类型安全的选项。

2. 关联值(Associated Values)

Swift 枚举的每个成员可以携带不同类型和数量的数据,这称为关联值

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

同一个枚举的不同成员可以携带完全不同的数据结构,这在做 API 返回结果建模时非常有用:

enum NetworkResult {
    case success(String)
    case failure(Error)
}

func fetchData() -> NetworkResult {
    .success("data from server")
}

3. 原始值(Raw Values)

如果你的枚举成员需要一个固定的基础值,可以用原始值。原始值类型在枚举定义中声明:

enum Season: String {
    case spring = "春暖花开"
    case summer = "夏日炎炎"
    case autumn = "秋高气爽"
    case winter = "冬日寒寒"
}

print(Season.summer.rawValue)
// 输出: 夏日炎炎

if let fall = Season(rawValue: "秋高气爽") {
    print("Found: \(fall)")
    // 输出: Found: autumn
}

注意 rawValue 反向查找返回的是可选值,因为传入的值可能不在枚举定义中。

4. CaseIterable 协议

让枚举遵守 CaseIterable 协议后,Swift 会生成 allCases 属性:

enum Planet: CaseIterable {
    case mercury, venus, earth, mars, jupiter
}

for planet in Planet.allCases {
    print("Planet: \(planet)")
}

这在需要遍历所有选项的场景(比如构建选择菜单、初始化默认状态)非常实用。

5. Switch 穷尽匹配

switch 语句在匹配枚举时要求覆盖所有情况。如果漏了某个 case,编译器会报错:

let homePlanet = Planet.earth

switch homePlanet {
case .mercury:
    print("Closest to the Sun")
case .venus:
    print("Hottest planet")
case .earth:
    print("Our home")
case .mars:
    print("The Red Planet")
case .jupiter:
    print("Largest planet")
}

为什么不需要 default

因为枚举只有这五个值,编译器能静态验证你覆盖了所有可能。省略 default 让新增枚举值时不会遗漏处理逻辑。

6. 递归枚举(Indirect)

当枚举的成员包含自身类型时,需要使用 indirect 关键字:

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case .number(let value):
        return value
    case .addition(let left, let right):
        return evaluate(left) + evaluate(right)
    case .multiplication(let left, let right):
        return evaluate(left) * evaluate(right)
    }
}

let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

print("Result: \(evaluate(product))")
// 输出: Result: 18

indirect 告诉 Swift 编译器把这个成员存储在堆上,避免无限递归的内存布局问题。这是实现语法树和表达式树的典型模式。

7. Optional 类型本身就是枚举

Swift 的 Optional<T> 其实就是一个标准库定义的枚举:

enum Optional<T> {
    case none
    case some(T)
}

nil 就是 .none,而 optionalValue! 就是 .some(wrapped) 的简写。理解这一点后,你就能明白为什么 Swift 的可选类型在 switch 中表现和枚举完全一致。


常见错误

错误 1: Switch 未覆盖所有情况

enum Color { case red, green, blue }

let color = Color.red
switch color {
case .red:
    print("Red")
case .green:
    print("Green")
// ❌ 漏了 .blue
}

编译器输出:

error: switch must be exhaustive and consider all possible cases

修复方法:

// 补全所有 case
case .blue:
    print("Blue")

错误 2: 忘记 indirect 关键字

enum Expression {
    case number(Int)
    case add(Expression, Expression)  // ❌
}

编译器输出:

error: enum case 'add' is indirectlyrecursive through 'Expression'

修复方法:

indirect enum Expression {  // ✅
    case number(Int)
    case add(Expression, Expression)
}

错误 3: RawValue 类型不匹配

enum Size: Int {
    case small = "S"      // ❌
    case medium = "M"
}

编译器输出:

error: raw value for enum case must be of type 'Int'

修复方法:

enum Size: String {       // ✅ 改为 String
    case small = "S"
    case medium = "M"
}

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
基本枚举Enum class 子类enum Name { A, B }enum Name { case A, B }Swift 需要 case 关键字
关联值无(用 dataclass 模拟)enum E { A(i32), B(String) }enum E { case a(Int), b(String) }Rust 和 Swift 都有关联值
原始值auto() 或手动赋值enum E: Type { A = val }enum E: Type { case A = "val" }语法相似,Swift 更严格
穷尽检查无运行时保证non-exhaustive pattern 警告编译时报错Swift 的编译器检查最严格
递归枚举无直接支持Box 间接引用indirect 关键字Swift 内置支持,无需手动装箱
枚举方法可以定义impl直接在 enum 内定义Swift 更简洁
Optional enumOptional[T] 在 typing 模块中Option<T>标准库 enum Optional<T>Swift/Rust 的 Optional 设计一致

动手练习

练习 1: 预测枚举匹配

不运行代码,预测下面代码的输出:

enum TrafficLight { case red, yellow, green }

let light = TrafficLight.yellow
switch light {
case .red:
    print("Stop")
case .yellow:
    print("Slow down")
case .green:
    print("Go")
}
点击查看答案

输出:

Slow down

解析: light 的值是 .yellow,命中第二个 case 分支。

练习 2: 定义一个关联值枚举

定义一个 Shape 枚举,包含 circle(带半径 Double)和 rectangle(带 width 和 height 两个 Double)。写一个 area() 方法计算面积。

点击查看参考实现
enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)

    func area() -> Double {
        switch self {
        case .circle(let radius):
            return .pi * radius * radius
        case .rectangle(let width, let height):
            return width * height
        }
    }
}

let c = Shape.circle(radius: 5.0)
print(c.area())  // 78.539...

练习 3: RawValue 查找

定义一个枚举 Language,原始值为字符串(中文表示:日语、英语、法语)。写代码通过 rawValue: "法语" 反向查找对应枚举值并打印。

点击查看参考实现
enum Language: String {
    case japanese = "日语"
    case english = "英语"
    case french = "法语"
}

if let lang = Language(rawValue: "法语") {
    print("Found: \(lang), value: \(lang.rawValue)")
}

故障排查 FAQ

Q: 关联值和原始值可以同时用吗?

A: 不行。一个枚举要么有关联值,要么有原始值,不能混用。原始值适合简单标识场景,关联值适合需要携带数据的场景。

Q: 为什么枚举不需要 default 分支?

A: 编译器能静态确认枚举的所有成员都被覆盖。这意味着将来有人新增了枚举值时,你的代码不会悄悄走 default,而是直接在编译时报错提醒你处理。

Q: indirect 关键字的性能影响大吗?

A: indirect 让 Swift 在堆上分配内存,相比栈上有略微开销,但在实际使用中完全可以忽略。这是实现递归数据结构的必要代价,和 Rust 的 Box 类似。


小结

核心要点:

  1. enum 定义枚举类型 — 成员通过 case 声明,彼此独立
  2. 关联值 — 让每个成员携带不同的附加数据
  3. 原始值 — 让枚举有基础类型(String/Int),支持 rawValue 读写
  4. CaseIterable — 自动生成 allCases 列表,方便遍历
  5. Switch 穷尽匹配 — 编译器确保所有 cases 都被覆盖

关键术语:

  • Enum: 枚举(一组类型安全的命名值)
  • Associated Value: 关联值(枚举成员附加的数据)
  • Raw Value: 原始值(枚举成员的基础固定值)
  • CaseIterable: 协议(自动生成 allCases
  • Indirect: 间接存储(支持递归枚举)
  • Exhaustive Matching: 穷尽匹配(覆盖所有枚举情况)

术语表

English中文
Enumeration枚举
Associated Value关联值
Raw Value原始值
CaseIterable可遍历协议
All Cases所有成员
Indirect间接存储
Exhaustive Matching穷尽匹配
Pattern Matching模式匹配
Optional Enumeration可选枚举
Member枚举成员

完整示例:Sources/BasicSample/EnumSample.swift


知识检查

问题 1 🟢(基础概念)

enum Direction { case up, down, left, right }
let d: Direction = .down

.down 的类型是什么?

A) String B) Int C) Direction D) Void

答案与解析

答案: C) Direction

解析: 枚举成员 .down 的类型就是它所属的枚举类型 Direction。枚举值不是字符串也不是整数。

问题 2 🟡(关联值理解)

enum Shape { case circle(Double), rectangle(width: Double, height: Double) }

以下哪个赋值正确?

A) Shape.circle(5) B) Shape.rectangle(width: 3.0, height: 4.0) C) A 和 B 都对

答案与解析

答案: C) A 和 B 都对

解析: 两个都是有效的关联值调用。circle 关联一个值,rectangle 关联两个命名元素。Swift 会根据调用上下文自动匹配。

问题 3 🔴(递归枚举)

indirect enum Expr {
    case value(Int)
    case add(Expr, Expr)
    case mul(Expr, Expr)
}

如果要表示表达式 (3 + 5) * 2,下列写法正确的是?

答案与解析

答案:

let expr = Expr.mul(
    Expr.add(Expr.value(3), Expr.value(5)),
    Expr.value(2)
)

解析: 递归枚举把子表达式作为关联值嵌入父节点。add 节点包含两个 value,外层 muladd 的结果乘以 2。这就是抽象语法树(AST)的典型表示方式。


延伸阅读

学习完枚举后,你可能还想了解:

选择建议:

  • 初学者 → 继续学习 结构体
  • 有经验的程序员 → 跳到 协议

记住:Swift 的枚举是真正的数据类型,不是简单的整数别名。它可以携带数据、定义方法、实现协议——这是 Swift 类型安全的基石。


继续学习

  • 下一步:结构体 — 值类型的数据模型
  • 相关:函数 — 把函数返回结果用枚举建模
  • 进阶:错误处理 — Result 枚举与 do-catch

结构体

开篇故事

想象你正在整理名片柜。每张名片记录一个人的信息:姓名、电话、邮箱。你把这张名片复印一份给朋友,朋友在上面改了电话号码。那张复印名片会更新,你原始的名片会跟着变吗?

不会。因为名片是复印件,修改复印件不会影响原件。这就是 Swift 中结构体(struct)的核心行为——值类型(value type)的语义:赋值和传递时,数据会被完整复制,彼此独立。理解这一点,你就掌握了 Swift 内存模型的一半。


本章适合谁

如果你希望学会如何自定义数据容器,管理一组相关字段,并理解 Swift 中值类型与引用类型的区别,本章适合你。结构体是 Swift 编程中最常用的自定义类型。


你会学到什么

完成本章后,你可以:

  1. 使用 struct 关键字定义自定义数据结构
  2. 区分存储属性(stored properties)和计算属性(computed properties)
  3. 使用属性观察器(willSet / didSet)监听属性变化
  4. 理解值类型(value type)的"按值复制"语义
  5. 掌握 mutating 关键字在结构体方法中的使用规则

前置要求

本章前置知识:阅读过 枚举,了解基本类型和函数定义。


第一个例子

打开 Sources/BasicSample/ClassSample.swift 中的 Matrix 结构体,找到最基础的定义:

struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
}

发生了什么?

  • struct Matrix — 定义一个结构体类型
  • let rows — 不可变存储属性
  • var grid — 可变存储属性
  • 结构体自带逐成员初始化器(memberwise initializer),无需手动编写

完整使用方式:

let m = Matrix(rows: 3, columns: 3)
print("Matrix size: \(m.rows) x \(m.columns)")

原理解析

1. 基本结构体定义

结构体通过 struct 关键字定义,包含属性(数据)和方法(行为):

struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]

    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        grid = Array(repeating: 0.0, count: rows * columns)
    }

    subscript(row: Int, column: Int) -> Double {
        get {
            grid[(row * columns) + column]
        }
        set {
            grid[(row * columns) + column] = newValue
        }
    }
}

结构体 Matrix 用一个一维的 Double 数组模拟二维矩阵,通过计算 row * columns + column 定位索引。subscript 语法让你可以用 matrix[row, column] 来访问元素。

2. 存储属性 vs 计算属性

存储属性直接保存值,计算属性不存储值,而是通过 get(可选 set)计算得到:

struct Rectangle {
    var width: Double
    var height: Double

    // 计算属性:不存储,通过 width 和 height 算出
    var area: Double {
        width * height
    }
}

let rect = Rectangle(width: 10, height: 5)
print(rect.area)  // 50.0

计算属性本质上是一对 get/set 方法,它不占用结构体的存储开销。

3. 属性观察器(willSet / didSet)

Swift 允许在属性值改变前后插入回调逻辑:

struct StepTracker {
    var steps: Int = 0 {
        willSet(newSteps) {
            print("About to change from \(steps) to \(newSteps)")
        }
        didSet {
            if steps > oldValue {
                print("Total steps now: \(steps)")
            }
        }
    }
}

var tracker = StepTracker()
tracker.steps = 100
tracker.steps += 1
  • willSet — 修改前触发,newValue 是即将设置的值
  • didSet — 修改后触发,oldValue 是旧的值

4. 值类型语义(Copy on Assignment)

结构体是值类型。赋值、传参时,数据会被完整复制:

struct Point {
    var x: Double, y: Double
}

var p1 = Point(x: 0, y: 0)
var p2 = p1   // p2 是 p1 的副本
p2.x = 100

print(p1.x)  // 0  — p1 没有受影响
print(p2.x)  // 100

这与类(class)的引用语义不同。类赋值时只复制引用,修改一处会影响所有引用。结构体则像复印件:彼此独立。

5. Mutating 方法

结构体的 func 方法默认不修改属性。如果要修改,必须加 mutating 关键字:

struct Counter {
    var count: Int = 0

    mutating func increment() {
        count += 1
    }

    static func zero() -> Counter {
        Counter(count: 0)
    }
}

var c = Counter()
c.increment()
print(c.count)  // 1

mutating 告诉 Swift:这个方法会修改 self。编译器会给这个方法一个可变的 self 副本并赋值回去。如果是 let 声明的结构体实例,则不允许调用 mutating 方法。

6. 静态属性和方法

static 定义属于类型本身而非实例的成员:

struct NetworkClient {
    static let defaultTimeout = 30.0

    static func createDefault() -> NetworkClient {
        // ...
    }
}

print(NetworkClient.defaultTimeout)  // 30.0

static 成员通过类型名访问,不依赖任何实例。这在定义常量、工厂方法时非常常见。

7. 结构体 vs 枚举:什么时候用哪个?

  • 用枚举:表示"一组互斥的选择",例如方向、状态、结果类型
  • 用结构体:表示"一组数据的容器",例如坐标、矩形、用户信息
  • 两者都是值类型,都是 Swift 推荐的默认选择(而非类)

常见错误

错误 1: 在 let 结构体上调用 mutating 方法

struct Point {
    var x: Double, y: Double
    mutating func move(dx: Double, dy: Double) {
        x += dx
        y += dy
    }
}

let p = Point(x: 0, y: 0)
p.move(dx: 10, dy: 20)  // ❌

编译器输出:

error: cannot use mutating member on immutable value: 'p' is a 'let' constant

修复方法:

var p = Point(x: 0, y: 0)  // 改为 var
p.move(dx: 10, dy: 20)  // ✅

错误 2: 结构体方法中修改属性但缺少 mutating

struct Counter {
    var count = 0
    func increment() {
        count += 1  // ❌
    }
}

编译器输出:

error: cannot assign to property: 'count' is immutable

修复方法:

mutating func increment() {  // ✅ 加 mutating
    count += 1
}

错误 3: 试图修改结构体计算属性但没有 setter

struct Rectangle {
    var width: Double, height: Double
    var area: Double { width * height }
}

var r = Rectangle(width: 3, height: 4)
r.area = 50  // ❌

编译器输出:

error: cannot assign to property: 'area' is a get-only property

修复方法:

要么接受只读结果,要么给 area 加一个 set 方法。


Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
结构体定义class Point: x, ystruct Point { x: f64, y: f64 }struct Point { var x: Double }Python 类默认引用类型
赋值语义引用复制Move / Copy(取决于 trait)完全复制(值类型)Swift 默认复制
计算属性@property 装饰器无(用 getter方法)var area: Double { get }Swift 语法最简洁
属性观察器willSet / didSet这是 Swift 独有的特性
可变方法修饰&mut selfmutating funcRust 用 borrow checker 静态保证
自动初始化器dataclass(装饰器)手动或宏 derive自动逐成员初始化器Swift 编译器自动生成
静态成员@staticmethod + @classmethodimpl Type { fn foo() }static func/let语法各有不同

动手练习

练习 1: 预测值类型行为

不运行代码,预测下面代码的输出:

struct Student {
    var name: String
    var score: Int
}

var s1 = Student(name: "Alice", score: 90)
var s2 = s1
s2.score = 100
print("\(s1.name): \(s1.score)")
print("\(s2.name): \(s2.score)")
点击查看答案

输出:

Alice: 90
Alice: 100

解析: s2 = s1 是副本赋值,修改 s2.score 不影响 s1。这就是值类型的行为。

练习 2: 写一个带计算属性的结构体

定义一个 Circle 结构体,包含 radius 属性。添加一个计算属性 diameter(等于半径 x2)。再添加一个 perimeter(周长 = 2 * pi * r)。

点击查看参考实现
struct Circle {
    var radius: Double

    var diameter: Double {
        radius * 2
    }

    var perimeter: Double {
        2 * .pi * radius
    }
}

let c = Circle(radius: 5)
print("Diameter: \(c.diameter)")    // 10
print("Perimeter: \(c.perimeter)")  // 31.4159...

练习 3: Mutating 实现 Toggle

定义一个 Switch 结构体,包含 isOn: Bool。写一个 mutating func toggle() 切换开关状态。

点击查看参考实现
struct Switch {
    var isOn: Bool

    mutating func toggle() {
        isOn.toggle()
    }
}

var power = Switch(isOn: false)
print(power.isOn)  // false
power.toggle()
print(power.isOn)  // true

故障排查 FAQ

Q: Swift 为什么推荐 struct 而非 class?

A: 值类型更安全。复制语义避免了意外的共享和副作用。Swift 标准库的类型(IntStringArray 等)全部是结构体,这是有意的设计抉择。只有在需要继承或多态行为时才考虑 class。

Q: mutating func 和类的方法有什么不同?

A: 结构体的 self 默认只读,所以修改属性的方法需要 mutating。类的 self 本来就是可变的(引用类型),不需要修饰符。

Q: 属性观察器 willSetdidSet 有什么区别?

A: willSet 在赋值之前触发,newValue 是即将设定的值。didSet 在赋值之后触发,oldValue 是旧的值。通常用 didSet 来响应变化做后续操作(比如刷新 UI)。


小结

核心要点:

  1. struct 定义值类型 — 赋值时复制数据,彼此独立
  2. 存储属性存数据let 不可变、var 可变
  3. 计算属性通过 getter 算出 — 不存储,不占用内存
  4. 属性观察器监听变化willSet / didSet 在修改前后触发
  5. mutating 允许修改 self — 结构体的方法默认不修改属性

关键术语:

  • Struct: 结构体(值类型自定义类型)
  • Stored Property: 存储属性(实际存储数据的字段)
  • Computed Property: 计算属性(通过计算得出,不存储)
  • Property Observer: 属性观察器(willSet/didSet
  • Value Type: 值类型(赋值时复制)
  • Mutating: 可变方法(允许修改 self)

术语表

English中文
Struct结构体
Value Type值类型
Stored Property存储属性
Computed Property计算属性
Property Observer属性观察器
willSet设置前回调
didSet设置后回调
Mutating可变(修饰符)
Memberwise Initializer逐成员初始化器
Static Member静态成员

完整示例:Sources/BasicSample/ClassSample.swiftMatrix 结构体部分)


知识检查

问题 1 🟢(基础概念)

struct Point { var x: Int, y: Int }
let p = Point(x: 1, y: 2)

p.x = 5 能编译通过吗?

A) 能 B) 不能

答案与解析

答案: B) 不能

解析: p 是用 let 声明的,整个结构体不可变,不能修改任何属性。改为 var p 即可。

问题 2 🟡(值类型理解)

struct Team { var name: String }
let t1 = Team(name: "Swift")
var t2 = t1
t2.name = "Rust"

t1.name 的值是什么?

答案与解析

答案: "Swift"

解析: t2 = t1 是值复制,t2t1 完全独立。修改 t2.name 不影响 t1.name

问题 3 🔴(进阶:计算属性与 didSet)

struct Celsius {
    var value: Double {
        didSet { value = max(0, value) }
    }
}

var temp = Celsius(value: -10)
print(temp.value)

输出是什么?为什么?

答案与解析

答案: 0.0

解析: didSet 在赋值后触发,把 value 修正为 max(0, -10)0.0。注意这实际上是 didSet 内部再次赋值,会引发二次 didSet 调用(但 max(0, 0) 不会改变值,不会无限递归)。这个例子展示了属性观察器的实际用法:数据校验与约束。


延伸阅读

学习完结构体后,你可能还想了解:

选择建议:

  • 初学者 → 继续学习 协议
  • 有经验的程序员 → 跳到 泛型

记住:Swift 中一切以值类型为首选。结构体、枚举让你写出无副作用的代码。类是最后的手段,只有在继承和多态真正需要时才使用。


继续学习

  • 下一步:协议 — 面向协议的编程范式
  • 相关:枚举 — 值类型兄弟,枚举表达选择
  • 进阶:类与继承 — 引用类型与继承机制

类与对象

开篇故事

想象你有一座积木工厂。你设计了一个基础积木模板,上面有凸起的点和连接槽。然后你在这个模板基础上,做出了不同形状的积木:带轮子的、带窗户的、带门的。它们都继承了基础积木的连接方式,但各自有不同的功能。

Swift 中的(Class)就跟这个模板工厂一样。你定义一个基础类,然后让其他类继承它的特性,再添加自己的独有功能。

在 Swift 中,类是引用类型(Reference Type)。这意味着当你把类实例赋值给另一个变量时,你传递的是指向同一个实例的"遥控器",而不是拷贝整个实例。这跟结构体(Struct)的值类型语义完全不同。


本章适合谁

如果你已经理解了变量、数据类型和函数,想深入学习 Swift 的面向对象编程,本章适合你。无论你是从 Python、Java 还是 Rust 过来,本章会帮你理解 Swift 类的独特设计。


你会学到什么

完成本章后,你可以:

  1. 使用 class 关键字定义类并使用继承
  2. 理解指定初始化器(Designated Initializer)和便捷初始化器(Convenience Initializer)
  3. 使用 deinit 管理资源清理和 ARC 内存管理
  4. 掌握引用类型与值类型的区别以及身份运算符 ===!==
  5. 使用 isas?as! 进行类型检查和类型转换

前置要求

确保你已经阅读了 基础数据类型 一章,理解 Swift 的基本类型系统和变量声明。本章中的类示例会用到字符串、数值和闭包等知识点。


第一个例子

打开 Sources/BasicSample/ClassSample.swift,找到以下代码:

/// MediaItem 基类
class MediaItem {
    var name: String
    init(name: String) {
        self.name = name
    }
}

/// Movie 继承 MediaItem
class Movie: MediaItem {
    var director: String
    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}

/// Song 也继承 MediaItem
class Song: MediaItem {
    var artist: String
    init(name: String, artist: String) {
        self.artist = artist
        super.init(name: name)
    }
}

发生了什么?

  • MediaItem 是基类,所有媒体项目的共同抽象,包含 name 属性
  • Movie 继承 MediaItem,额外增加了 director(导演)属性
  • Song 同样继承 MediaItem,增加了 artist(艺术家)属性
  • 子类初始化器必须先初始化自己的属性,再调用 super.init 初始化父类

使用示例

let movie = Movie(name: "Blade Runner", director: "Ridley Scott")
let song = Song(name: "Imagine", artist: "John Lennon")
print("\(movie.name) directed by \(movie.director)")
print("\(song.name) by \(song.artist)")

输出

Blade Runner directed by Ridley Scott
Imagine by John Lennon

原理解析

1. 类定义与继承

Swift 中的继承使用冒号语法。子类自动获得父类的所有属性和方法:

class Shape {
    var description: String {
        "Shape"
    }
    var area: Double { 0.0 }
}

class Rectangle: Shape {
    var width: Double
    var height: Double

    init(width: Double, height: Double) {
        self.width = width
        self.height = height
    }

    override var area: Double {
        get {
            return width * height
        }
        set(newArea) {
            height = newArea / width
        }
    }

    override var description: String { "Rectangle" }
}

关键点

  • 子类用 class Rectangle: Shape 语法继承
  • 子类必须用 override 关键字标记对父类的重写
  • Swift 不会默认允许重写,必须显式声明,这比 Java 或 C++ 更安全

2. 指定初始化器 vs 便捷初始化器

每个类至少有一个指定初始化器(Designated Initializer),负责确保所有属性都被正确初始化。Swift 还支持便捷初始化器(Convenience Initializer):

class Person {
    var firstName: String
    var lastName: String
    var age: Int

    // 指定初始化器 — 必须初始化所有属性
    init(firstName: String, lastName: String, age: Int) {
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
    }

    // 便捷初始化器 — 委托给指定初始化器
    convenience init(firstName: String, lastName: String) {
        self.init(firstName: firstName, lastName: lastName, age: 0)
    }
}

let alice = Person(firstName: "Alice", lastName: "Wong") // 使用便捷初始化器
let bob = Person(firstName: "Bob", lastName: "Lee", age: 30) // 使用指定初始化器

初始化规则

  1. 指定初始化器必须初始化本类声明的所有属性,然后调用父类的初始化器
  2. 便捷初始化器必须委托给同一个类的另一个初始化器
  3. 子类不会自动继承父类的初始化器,除非满足特定条件

3. 重写(Override)

子类可以重写父类的属性、方法和下标。Swift 要求使用 override 关键字:

class Circle: Shape {
    var radius: Double

    init(radius: Double) {
        self.radius = radius
    }

    // 重写父类的只读计算属性
    override var area: Double {
        return .pi * radius * radius
    }

    override var description: String { "Circle" }
}

let circle = Circle(radius: 5.0)
print("\(circle.description) area = \(circle.area)")
// 输出: Circle area = 78.53981633974483

注意事项

  • override 是必须的,不加 override 的匹配方法签名会报编译错误
  • 重写后方法的访问级别不能比父类更严格
  • 可以用 final 关键字阻止子类重写

4. deinit 与 ARC 内存管理

Swift 使用自动引用计数(Automatic Reference Counting, ARC)管理内存。当类的引用计数归零时,deinit 会被自动调用:

class DatabaseConnection {
    let connectionString: String

    init(connectionString: String) {
        self.connectionString = connectionString
        print("Connected to \(connectionString)")
    }

    deinit {
        print("Disconnected from \(connectionString)")
        // 清理数据库连接等资源
    }
}

func createConnection() {
    let db = DatabaseConnection(connectionString: "localhost:5432/mydb")
    print("Working with database...")
}
// 函数结束后 db 引用归零,deinit 自动调用

createConnection()

输出

Connected to localhost:5432/mydb
Working with database...
Disconnected from localhost:5432/mydb

ARC 核心规则

  • 每次有新的引用指向一个类实例,引用计数 +1
  • 每次引用离开作用域或被设为 nil,引用计数 -1
  • 引用计数归零时,ARC 自动调用 deinit 并释放内存
  • 结构体和枚举是值类型,不受 ARC 管理

5. 引用类型 vs 值类型

这是 Swift 最重要的概念之一。类是引用类型,结构体和枚举是值类型

class MyClass {
    var value = 0
}

struct MyStruct {
    var value = 0
}

// 值类型:赋值时拷贝
var a = MyStruct(value: 10)
var b = a            // 拷贝一份新的
b.value = 20         // 修改 b
print(a.value)        // 10 — a 没有被影响

// 引用类型:赋值时共享
let x = MyClass()
x.value = 10
let y = x            // y 指向同一个实例
y.value = 20         // 修改 y
print(x.value)        // 20 — x 也被影响了!

选择建议

场景推荐类型原因
需要共享状态、身份语义类 (Class)引用语义天然支持共享
数据模型、值语义结构体 (Struct)拷贝安全,线程安全
继承和多态类或协议类支持继承,协议支持 POP
不需要生命周期管理结构体或枚举无需 ARC、deinit

6. 身份运算符 === 和 !==

引用类型有"身份"概念。两个变量可能指向同一个实例,也可能指向内容相同但不同的实例:

class User {
    let id: Int
    init(id: Int) { self.id = id }
}

let user1 = User(id: 1)
let user2 = User(id: 1)
let user3 = user1

print(user1 === user2)  // false — 两个不同的实例 (内容相同)
print(user1 === user3)  // true  — 同一个实例
print(user1 !== user2)  // true  — user1 和 user2 不是同一个实例

身份运算符 vs 相等运算符

  • === 检查两个引用是否指向同一个实例(指针比较)
  • == 检查两个值是否逻辑相等(需要实现 Equatable 协议)

7. 类型检查与类型转换

Swift 使用 isas?as! 进行类型检查和转换:

let library: [MediaItem] = [
    Movie(name: "Inception", director: "Christopher Nolan"),
    Song(name: "Bohemian Rhapsody", artist: "Queen"),
    Movie(name: "Interstellar", director: "Christopher Nolan")
]

// is — 类型检查
var movieCount = 0
var songCount = 0
for item in library {
    if item is Movie {
        movieCount += 1
    } else if item is Song {
        songCount += 1
    }
}
print("Library contains \(movieCount) movies and \(songCount) songs")

// as? — 条件类型转换(返回 Optional)
for item in library {
    if let movie = item as? Movie {
        print("Movie: \(movie.name), director: \(movie.director)")
    }
}

// as! — 强制类型转换(可能崩溃!)
let firstItem = library[0]
let definitelyMovie = firstItem as! Movie  // 如果实际不是 Movie 会崩溃

类型转换安全建议

  • 优先使用 as?,它返回 Optional,失败时是 nil 而不是崩溃
  • 只在确定类型匹配时使用 as!
  • as 用于已知安全的编译时转换,如 Bridging Cast(Swift 类型与 Foundation 类型之间)

8. 弱引用与无主引用

ARC 的一个常见问题是强引用循环。当两个类实例互相持有对方的强引用时,它们的引用计数永远不会归零,导致内存泄漏:

// 问题示例:强引用循环
class Department {
    let name: String
    var courses: [Course] = []    // 强引用
    init(name: String) { self.name = name }
    deinit { print("\(name) deinitialized") }
}

class Course {
    let title: String
    weak var department: Department?  // 弱引用,不增加引用计数
    init(title: String) { self.title = title }
    deinit { print("\(title) deinitialized") }
}

// 使用弱引用打破循环
let dept = Department(name: "Computer Science")
let course = Course(title: "Data Structures")
dept.courses.append(course)
course.department = dept  // weak 引用,不阻止 dept 被释放

weak vs unowned

关键字引用计数可以为 nil使用场景
weak不增加✅ 可以生命周期可能比持有者短
unowned不增加❌ 不可以生命周期与持有者相同
默认 (强引用)增加取决于类型一般情况

常见错误

错误 1: 子类没有在父类属性初始化前调用 super.init

class Parent {
    let value: Int
    init(value: Int) { self.value = value }
}

class Child: Parent {
    let extra: String
    init(value: Int, extra: String) {
        super.init(value: value)  // ❌ 先调 super.init,但 extra 还没初始化
        self.extra = extra
    }
}

编译器输出

error: 'self' used in property access 'value' before super init initializes self

修复方法

class Child: Parent {
    let extra: String
    init(value: Int, extra: String) {
        self.extra = extra        // ✅ 先初始化子类属性
        super.init(value: value)   // 再调用父类初始化器
    }
}

错误 2: 不使用 override 关键字重写父类方法

class Shape {
    func draw() { print("Drawing shape") }
}

class Circle: Shape {
    func draw() { print("Drawing circle") } // ❌ 缺少 override
}

编译器输出

error: method 'draw()' in non-final class 'Circle' must be explicitly declared with 'override'

修复方法

class Circle: Shape {
    override func draw() { print("Drawing circle") } // ✅ 加上 override
}

错误 3: 强制类型转换失败导致运行时崩溃

let items: [MediaItem] = [Song(name: "Yesterday", artist: "The Beatles")]
let movie = items[0] as! Movie  // ❌ 运行时崩溃!Song 不是 Movie

运行时崩溃输出

fatal error: unexpectedly found nil while unwrapping an Optional value
// 或
Could not cast value of type 'Song' to 'Movie'

修复方法

if let movie = items[0] as? Movie {  // ✅ 使用条件转换
    print(movie.name)
} else {
    print("Not a movie")
}

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
类定义class Foo:无(用 struct + impl)class Foo { }Rust 没有类,只有结构体
继承支持多继承无(用 Trait)单继承Swift/Rust 都倾向组合优于继承
引用计数自动(CPython 内部)手动(Rc/Arc)自动(ARC)Swift ARC 编译期插入 retain/release
可变性默认可变默认不可变(let/mut引用可变(属性可标 var)Python 类实例默认可变
析构函数__del__Drop traitdeinitSwift deinit 不接受参数
类型转换isinstance()dyn Trait + downcastis, as?, as!Swift 提供安全的 Optional 转换
身份比较isRc::ptr_eq===, !==Python/Rust 都有对应方案

动手练习

练习 1: 创建类层次结构

设计一个动物类层次结构:

  • 基类 Animal,包含名称和发出声音的方法
  • 子类 DogCat,各自实现不同的叫法
  • 创建一个 [Animal] 数组,遍历并让每个动物发出声音
点击查看答案
class Animal {
    let name: String
    init(name: String) { self.name = name }
    func makeSound() -> String { "..." }
}

class Dog: Animal {
    override func makeSound() -> String { "Woof!" }
}

class Cat: Animal {
    override func makeSound() -> String { "Meow!" }
}

let animals: [Animal] = [Dog(name: "Buddy"), Cat(name: "Whiskers")]
for animal in animals {
    print("\(animal.name): \(animal.makeSound())")
}
// 输出:
// Buddy: Woof!
// Whiskers: Meow!

练习 2: 类型转换统计

给定一个混合数组包含 MovieSongMediaItem,使用类型转换统计每种类型的数量:

let items: [MediaItem] = [
    Movie(name: "A", director: "X"),
    Song(name: "B", artist: "Y"),
    Movie(name: "C", director: "Z"),
    MediaItem(name: "D"),
    Song(name: "E", artist: "W")
]
点击查看答案
var movieCount = 0, songCount = 0, baseCount = 0

for item in items {
    if item is Movie {
        movieCount += 1
    } else if item is Song {
        songCount += 1
    } else {
        baseCount += 1
    }
}

print("Movies: \(movieCount), Songs: \(songCount), Base: \(baseCount)")
// 输出: Movies: 2, Songs: 2, Base: 1

练习 3: 修复引用循环

下面的代码会导致内存泄漏,请用 weakunowned 修复:

class Student {
    let name: String
    var school: School
    init(name: String, school: School) {
        self.name = name
        self.school = school
    }
}

class School {
    let name: String
    var students: [Student] = []
    init(name: String) { self.name = name }
}
点击查看答案
class Student {
    let name: String
    unowned let school: School  // ✅ 用 unowned:学生存续期间学校一定存在
    init(name: String, school: School) {
        self.name = name
        self.school = school
    }
}

class School {
    let name: String
    var students: [Student] = []
    init(name: String) { self.name = name }
}

说明:这里用 unowned 是合理的,因为 Student 引用 School 时,School 一定存活(先创建 School 再把学生加入)。如果用 weak 也可以,但每次访问都要解包 Optional。


故障排查 FAQ

Q: 什么时候应该使用类而不是结构体?

A: 遵循 Swift 社区的共识:

  • 默认使用结构体 - 大部分情况下值语义更安全、更简单
  • 需要引用语义时使用类 - 需要共享同一份数据,或需要身份概念时
  • 需要继承时使用类 - 虽然协议(Protocol)通常比继承更灵活
  • 需要 deinit 时只能用类 - 结构体没有析构函数

参考项目中的选择:ClassSample.swift 中的 MediaItem 体系用类是因为需要多态和共享身份;而 Matrix 用结构体是因为它是纯数据模型。

Q: weakunowned 到底怎么选?

A: 问自己一个问题:被引用的对象可能先于引用者变成 nil 吗?

  • → 用 weak(声明为 var weak var,Optional 类型)
  • 不会 → 用 unowned(声明为 unowned let/var,非 Optional,访问已释放的实例会崩溃)

常见场景:Delegate 用 weak,父子关系(子持有父)用 unowned

Q: 为什么 Swift 的类只支持单继承?

A: Swift 选择单继承是因为:

  • 避免菱形问题 - 多继承的歧义和复杂性
  • 协议(Protocol)提供多继承的效果 - 一个类型可以实现多个协议
  • 组合优于继承 - 通过协议扩展(Protocol Extension)实现默认实现,比继承更灵活
  • Rust 甚至完全没有继承,只用 Trait,证明了单继承 + Trait/协议是更干净的设计

小结

核心要点

  1. 类是引用类型 - 赋值和传参时共享同一个实例,而不是拷贝
  2. 继承用冒号 - class Child: Parent,子类用 override 重写父类成员
  3. ARC 自动管理内存 - 引用计数归零时自动释放,deinit 用于清理资源
  4. weakunowned 打破强引用循环 - 避免内存泄漏
  5. 类型转换用 isas? - 安全地检查并转换类型,避免使用 as!

关键术语

  • Class: 类(引用类型)
  • Inheritance: 继承(子类获得父类特性)
  • Designated Initializer: 指定初始化器(主要初始化器)
  • Convenience Initializer: 便捷初始化器(辅助初始化器)
  • ARC: 自动引用计数(Automatic Reference Counting)
  • deinit: 析构器(对象销毁时调用)
  • Type Casting: 类型转换(运行时检查并转换类型)

术语表

English中文
Class
Inheritance继承
Subclass子类
Superclass / Parent class父类
Designated Initializer指定初始化器
Convenience Initializer便捷初始化器
Override重写
ARC (Automatic Reference Counting)自动引用计数
deinit析构器
Reference Type引用类型
Value Type值类型
Identity Operator身份运算符
Type Casting类型转换
Weak Reference弱引用
Unowned Reference无主引用
Strong Reference Cycle强引用循环

完整示例:Sources/BasicSample/ClassSample.swift


知识检查

问题 1 🟢 (基础概念)

class Counter {
    var count = 0
    func increment() { count += 1 }
}

let c1 = Counter()
let c2 = c1
c1.increment()
print(c2.count)

输出是什么?

A) 0
B) 1
C) 编译错误
D) 运行时错误

答案与解析

答案: B) 1

解析: c1c2 指向同一个 Counter 实例。c1.increment() 修改了共享实例的 count 属性,c2 看到的也是同样的值。这是引用类型的核心特性。

问题 2 🟡 (初始化顺序)

class Parent {
    let x: Int
    init(x: Int) { self.x = x }
}

class Child: Parent {
    let y: String
    init(x: Int, y: String) {
        super.init(x: x)  // 行 A
        self.y = y        // 行 B
    }
}

这段代码能通过编译吗?

A) 能
B) 不能,行 A 和行 B 需要交换位置
C) 不能,需要 convenience init
D) 不能,x 必须用 var

答案与解析

答案: B) 不能,行 A 和行 B 需要交换位置

解析: Swift 的两阶段初始化规则要求:子类必须先初始化自己的属性(self.y = y),再调用父类初始化器(super.init)。所以行 A 和行 B 需要交换。交换后:

init(x: Int, y: String) {
    self.y = y           // 先初始化子类属性
    super.init(x: x)     // 再调用父类初始化器
}

问题 3 🔴 (ARC 与内存管理)

class Node {
    let value: Int
    var next: Node?

    init(value: Int) { self.value = value }
    deinit { print("Node \(value) freed") }
}

var n1 = Node(value: 1)
var n2 = Node(value: 2)
n1.next = n2
n2.next = n1  // 强引用循环

n1 = nil
n2 = nil

两个节点的 deinit 会被调用吗?

A) 会,两个都被正常释放
B) 不会,强引用循环导致内存泄漏
C) 只释放 n1
D) 编译错误

答案与解析

答案: B) 不会,强引用循环导致内存泄漏

解析: n1 持有 n2 的强引用,n2 也持有 n1 的强引用,形成循环。即使外部引用被设为 nil,两个节点的引用计数仍然是 1,永远不会归零,ARC 不会释放它们。

修复:将其中一个引用改为 weak

class Node {
    let value: Int
    weak var next: Node?   // ✅ 弱引用
    init(value: Int) { self.value = value }
    deinit { print("Node \(value) freed") }
}

延伸阅读

学习完类与对象后,你可能还想了解:

选择建议:

  • 初学者 → 继续学习 协议,理解 Protocol-Oriented Programming
  • 有面向对象编程经验 → 跳到 泛型

💡 记住:Swift 默认推荐结构体,类是在需要引用语义时才选择的高级工具。不要滥用继承,优先考虑协议和组合。


继续学习

  • 下一步:协议 - 理解 Swift 的 Protocol-Oriented Programming 范式
  • 相关:泛型 - 编写类型安全的通用代码
  • 进阶:错误处理 - do/catch/try 错误传递机制

协议

开篇故事

想象你开了一家餐厅。你不需要知道每个厨师来自哪里,受过什么训练,甚至不需要他们是正式员工还是临时工。你只需要确保:每个厨师都能按照菜谱做菜,都能在规定时间完成出餐,都能保证菜品卫生。

Swift 中的协议(Protocol)就是这份"菜谱"。它定义了一组要求,任何类型只要满足这些要求,就能被接受。结构体可以遵守协议,类可以遵守协议,枚举甚至可以扩展已有类型来遵守协议。

协议是 Swift 区别于其他语言的核心特性之一。Swift 倡导面向协议编程(Protocol-Oriented Programming, POP),而不是传统的类继承体系。这种设计让代码更灵活、更可组合、更容易测试。


本章适合谁

如果你理解了类、继承和值类型的概念,想理解 Swift 更灵活的抽象方式,本章适合你。如果你来自 Java 或 C#,你会发现 Swift 的协议比接口更强大。如果你来自 Rust,你会发现 Swift 协议和 Trait 有很多相似之处,但也有自己的特点。


你会学到什么

完成本章后,你可以:

  1. 使用 protocol 关键字定义协议并让类型遵守
  2. 理解协议方法、属性要求和 mutating 关键字
  3. 使用协议扩展(Protocol Extension)提供默认实现
  4. 掌握协议组合(Protocol Composition)和 some 关键字
  5. 理解面向协议编程(POP)与类继承的区别

前置要求

确保你已经阅读了 类与对象 一章,理解类、结构体、继承和 Swift 的基本类型系统。本章讨论的协议概念是在这些基础之上的更高级抽象。


第一个例子

打开 Sources/BasicSample/ExampleProtocol.swift,看最基础的定义:

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust()
}

发生了什么?

  • protocol 关键字定义协议
  • var simpleDescription: String { get } 声明一个只读属性要求
  • mutating func adjust() 声明一个会修改自身的方法要求

不同的类型可以以自己的方式遵守这个协议:

/// 结构体遵守协议
struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}

/// 类遵守协议
class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class."
    func adjust() {  // 注意:类不需要 mutating
        simpleDescription += "  Now 100% adjusted."
    }
}

使用示例

var structure = SimpleStructure()
print(structure.simpleDescription)
structure.adjust()
print(structure.simpleDescription)

let classInstance = SimpleClass()
print(classInstance.simpleDescription)
classInstance.adjust()
print(classInstance.simpleDescription)

输出

A simple structure
A simple structure (adjusted)
A very simple class.
A very simple class.  Now 100% adjusted.

原理解析

1. 协议定义与基本遵守

协议定义类型必须满足的一组要求。一个类型可以同时遵守多个协议:

protocol Describable {
    var description: String { get }
}

protocol Identifiable {
    var id: Int { get set }
}

// 同时遵守两个协议
struct Product: Describable, Identifiable {
    let name: String
    var id: Int
    var description: String { "Product #\(id): \(name)" }
}

let product = Product(name: "Widget", id: 42)
print(product.description)  // "Product #42: Widget"

协议要求包括

  • 实例属性和类型属性(varstatic var
  • 实例方法和类型方法(funcstatic func
  • 下标(subscript
  • 协议本身也可以继承其他协议

2. mutating 关键字

mutating 是协议方法要求中的一个特殊关键字。它告诉编译器:这个方法会修改调用它的实例。

protocol Toggleable {
    var isOn: Bool { get set }
    mutating func toggle()  // 会修改自身
}

// 结构体需要 mutating
struct LightSwitch: Toggleable {
    var isOn: Bool = false
    mutating func toggle() {
        isOn = !isOn
    }
}

// 类不需要 mutating(类方法默认可以修改属性)
class Lamp: Toggleable {
    var isOn: Bool = false
    func toggle() {  // 不需要 mutating
        isOn = !isOn
    }
}

为什么需要 mutating?

因为 Swift 中结构体和枚举是值类型,默认方法不能修改自身属性。mutating 明确标记"这个方法会替换掉 self"。对于类来说,它是引用类型,方法本来就可以修改属性,所以不需要 mutating

3. 协议扩展与默认实现

协议的真正威力在于协议扩展(Protocol Extension)。你可以为协议方法提供默认实现,让遵守者可以选择不覆盖:

protocol Animal {
    func makeSound() -> String
    func greet() -> String
}

// 给 greet 提供默认实现
extension Animal {
    func greet() -> String {
        return "The animal says: \(makeSound())"
    }
}

struct Dog: Animal {
    func makeSound() -> String { "Woof" }
    // greet() 使用默认实现
}

struct Cat: Animal {
    func makeSound() -> String { "Meow" }
    // 覆盖默认实现
    func greet() -> String {
        return "Cat: \(makeSound())! *purrs*"
    }
}

print(Dog().greet())  // "The animal says: Woof"
print(Cat().greet())   // "Cat: Meow! *purrs*"

协议扩展 vs 类继承

特性类继承协议扩展
支持的类型仅类类、结构体、枚举
多继承不支持(单继承)可以实现多个协议
已有类型扩展不能扩展没有源码的类可以扩展任何类型
默认实现在父类提供在协议扩展提供
分发方式动态分发(虚函数表)静态分发(更快)

4. 协议作为类型(多态)

协议本身也是一个类型。这意味着你可以把协议用于变量声明、函数参数、数组元素等:

protocol Drawable {
    func draw() -> String
}

struct Circle: Drawable {
    func draw() -> String { "  ○  " }
}

struct Square: Drawable {
    func draw() -> String { "  ■  " }
}

// 协议作为数组元素类型
let shapes: [Drawable] = [Circle(), Square(), Circle()]

for shape in shapes {
    print(shape.draw())
}

这就是 Swift 的协议多态(Protocol Polymorphism)。它和类继承的多态效果一样,但不依赖继承体系。

来自项目中的实际应用:

// ExampleProtocol.swift 中对 Int 的扩展
extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
}

// 现在所有整数都实现了 ExampleProtocol
let x: ExampleProtocol = 7
print(x.simpleDescription)  // "The number 7"

5. associatedtype 关联类型

关联类型让协议可以定义一个"占位符类型",具体类型由遵守者决定:

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
    mutating func append(_ item: Item)
}

struct IntStack: Container {
    typealias Item = Int  // 关联类型设为 Int
    var items: [Int] = []
    var count: Int { items.count }
    subscript(i: Int) -> Int { items[i] }
    mutating func append(_ item: Int) {
        items.append(item)
    }
}

struct StringStack: Container {
    typealias Item = String  // 关联类型设为 String
    var items: [String] = []
    var count: Int { items.count }
    subscript(i: Int) -> String { items[i] }
    mutating func append(_ item: String) {
        items.append(item)
    }
}

关联类型 vs 泛型

  • 泛型参数在定义时指定(如 Array<Int>
  • 关联类型由类型的实现决定,使用者不需要显式指定

Swift 标准库中大量使用关联类型。比如 Sequence 协议就有 Element 关联类型,所以 for-in 循环能适用于 ArrayDictionaryString 等所有序列类型。

6. 协议组合

Sometimes you want a value to conform to multiple protocols simultaneously. Swift uses the & operator for protocol composition:

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

struct Person: Named, Aged {
    let name: String
    let age: Int
}

// 协议组合作为参数类型
func printInfo(_ person: Named & Aged) {
    print("\(person.name), age \(person.age)")
}

let bob = Person(name: "Bob", age: 30)
printInfo(bob)  // "Bob, age 30"

协议组合不创建新类型,它只是一个临时的"同时满足多个协议"约束。

7. some 关键字(不透明类型)

Swift 5.1 引入了 some 关键字,让函数可以返回"符合某个协议的某个具体类型",而不暴露具体类型:

protocol Shape {
    func area() -> Double
}

struct Triangle: Shape {
    let base: Double
    let height: Double
    func area() -> Double { base * height / 2 }
}

// some Shape = 返回某个遵守 Shape 的具体类型,但调用者不知道是什么
func makeDefaultShape() -> some Shape {
    Triangle(base: 10, height: 5)
}

// 调用者只能用 Shape 定义的接口
let shape = makeDefaultShape()
print(shape.area())  // 25.0
// print(shape.base)  // ❌ 编译错误!调用者不知道底层类型

some vs 协议类型的区别

// 协议类型 — 可以是任意遵守协议的类型的混合
func describe1() -> Shape {  // 返回类型可以是任何 Shape
    Triangle(base: 10, height: 5)
}
// 每次调用可以返回不同类型

// some 关键字 — 必须是某个具体类型
func describe2() -> some Shape {  // 返回类型固定,但隐藏
    Triangle(base: 10, height: 5)
}
// 每次调用必须返回相同的具体类型
// 编译器能在编译期知道具体类型,可以做更好的优化

some 关键字在 SwiftUI 中大量使用,View 协议返回值的标准写法就是 some View

8. 面向协议编程 vs 类继承

维度类继承面向协议编程(POP)
适用类型仅类类、结构体、枚举
多重继承不支持通过多协议实现
默认行为父类方法协议扩展默认实现
已有类型扩展不能可以(扩展 Int、String 等)
内存模型引用类型可选值类型或引用类型
分发动态(运行时)静态(编译时,更快)
测试性需要 mock 父类更容易 mock(值类型)

Swift 标准库几乎完全基于协议构建:EquatableComparableHashableSequenceCollectionCodable ...... 这些协议让你的自定义类型一键获得丰富的功能。


常见错误

错误 1: 结构体中忽略 mutating 关键字

protocol Counter {
    func increment()
}

struct SimpleCounter: Counter {
    var count = 0
    func increment() {  // ❌ 缺少 mutating
        count += 1
    }
}

编译器输出

error: cannot assign to property: 'self' is immutable
note: protocol requires function 'increment()' with 'mutating' modifier

修复方法

struct SimpleCounter: Counter {
    var count = 0
    mutating func increment() {  // ✅ 加 mutating
        count += 1
    }
}

错误 2: 协议作为返回类型时混用不同类型

protocol Shape {
    func draw() -> String
}

struct Circle: Shape { func draw() -> String { "○" } }
struct Square: Shape { func draw() -> String { "■" } }

func randomShape() -> Shape {
    if Bool.random() {
        return Circle()
    } else {
        return Square()   // ✅ 这可以 — 返回协议类型允许混用
    }
}

但如果用 some 关键字就不行

func randomShape() -> some Shape {  // ❌ some 要求返回具体同一类型
    if Bool.random() {
        return Circle()
    } else {
        return Square()  // 编译错误!
    }
}

编译器输出

error: function declares an opaque return type, but the return statements
       in its body do not have matching underlying types

修复方法:用协议类型(Shape)而不是不透明类型(some Shape):

func randomShape() -> Shape {  // ✅ 协议类型允许混用
    if Bool.random() {
        return Circle()
    } else {
        return Square()
    }
}

错误 3: 协议中含有 associatedtype 不能直接用作类型

protocol Container {
    associatedtype Item
    func count() -> Int
}

// ❌ 不能直接用作类型 — 编译器不知道 Item 是什么
let container: Container = ...

编译器输出

error: protocol 'Container' can only be used as a generic constraint
       because it has Self or associated type requirements

修复方法:用泛型或 some 关键字约束:

// 泛型约束
func useContainer<C: Container>(_ c: C) {
    print(c.count())
}

// 或者 some 关键字
func makeContainer() -> some Container {
    // 返回某个具体的 Container 实现
}

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
协议/接口ABC(抽象基类)/ Protocol (typing)TraitProtocolSwift 协议可以有默认实现
扩展已有类型不能(需猴子补丁)可以(impl Trait for Type)可以(extension Type: Protocol)Swift 和 Rust 都支持
多继承支持不支持(实现多个 Trait)不支持(遵守多个协议)Swift 协议组合用 &
关联类型Associated TypeassociatedtypeSwift/Rust 高度相似
不透明返回impl Traitsome Protocol等价功能
分发方式动态静态(泛型)或动态(dyn)静态(泛型/some)或动态(协议)Swift some 用静态分发
mutatingself 总是可变&mut selfmutating(值类型)概念类似,语法不同

动手练习

练习 1: 协议的基本遵守

定义一个 Mathable 协议,要求有一个 calculate() → Double 方法。让 RectangleCircle 两个结构体遵守它,各自实现面积计算。

点击查看答案
protocol Mathable {
    func calculate() -> Double
}

struct Rectangle: Mathable {
    let width: Double
    let height: Double
    func calculate() -> Double {
        width * height
    }
}

struct Circle: Mathable {
    let radius: Double
    func calculate() -> Double {
        .pi * radius * radius
    }
}

let rect = Rectangle(width: 5, height: 3)
let circle = Circle(radius: 4)
print(rect.calculate())    // 15.0
print(circle.calculate())   // 50.26548245743669

练习 2: 协议扩展默认实现

扩展上题的 Mathable 协议,添加一个 description() 方法,默认返回 "Area: X" 格式的字符串。让 Rectangle 覆盖它,返回 "Rectangle: width x height"。

点击查看答案
extension Mathable {
    func description() -> String {
        "Area: \(calculate())"
    }
}

extension Rectangle {
    func description() -> String {
        "Rectangle: \(width) x \(height)"
    }
}

let rect = Rectangle(width: 5, height: 3)
let circle = Circle(radius: 4)
print(rect.description())   // "Rectangle: 5.0 x 3.0" (覆盖实现)
print(circle.description())  // "Area: 50.26548245743669" (默认实现)

练习 3: 关联类型容器

实现一个 Stack 类型,遵守以下协议协议定义了 pushpoppeek 方法:

protocol Storable {
    associatedtype Item
    mutating func push(_ item: Item)
    mutating func pop() -> Item?
    func peek() -> Item?
}
点击查看答案
struct Stack<Item>: Storable {
    private var items: [Item] = []

    mutating func push(_ item: Item) {
        items.append(item)
    }

    mutating func pop() -> Item? {
        items.popLast()
    }

    func peek() -> Item? {
        items.last
    }
}

// 使用
var stack = Stack<Int>()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.peek() ?? "nil")  // Optional(3)
print(stack.pop() ?? "nil")   // 3
print(stack.pop() ?? "nil")   // 2

故障排查 FAQ

Q: 协议(Protocol)和类继承,我应该选哪个?

A: Swift 社区的共识是:优先考虑协议。

  • 优先用协议 + 结构体 - 值类型 + 协议组合更安全、更可测试
  • 需要共享状态或身份时 - 用类(比如 UIView 的子类化)
  • 需要扩展已有库的类型 - 只能用协议,不能继承没有源码的类
  • 多态需求 - 协议比继承更灵活,一个类型可以遵守多个协议

Apple 自己的框架也越来越多地采用协议。SwiftUI 的 View 协议就是一个协议,而不是基类。

Q: some Protocolprotocol 类型作为返回值有什么区别?

A: 关键区别在于类型确定性和性能:

  • some Protocol(不透明类型)— 编译器知道具体类型,可以做内联优化,但所有返回路径必须是同一具体类型
  • protocol(协议类型)— 运行时动态分发,允许不同返回路径,但有一点性能开销

如果你每次返回相同的类型,用 some。如果你需要返回不同类型的值,用协议类型。

Q: associatedtype 和泛型参数有什么区别?

A: 它们解决的问题不同:

  • 泛型参数 — 调用者决定。比如 Array<Int>,你告诉 Array 元素的类型
  • 关联类型 — 实现者决定。比如你实现的 Container,你把 Item 设为什么就是什么,使用者不用管

类比思考:泛型参数像是菜单上点菜(客人选),关联类型像是套餐配什么菜(厨师定)。


小结

核心要点

  1. 协议定义类型要求 - 属性、方法、下标可以成为协议要求
  2. 协议扩展提供默认实现 - 让遵守者可选覆盖,类似"继承"但不限制类型
  3. 协议作为类型使用 - 实现多态,支持 [Protocol] 数组和协议参数
  4. associatedtype 定义占位类型 - 由遵守者决定具体类型
  5. some Protocol 是不透明返回类型 - 隐藏具体类型但保留编译期类型信息

关键术语

  • Protocol: 协议(定义一组要求)
  • Conformance: 遵守(类型满足协议要求)
  • Protocol Extension: 协议扩展(为协议提供默认实现)
  • Associated Type: 关联类型(协议中的占位类型)
  • Protocol Composition: 协议组合(同时满足多个协议)
  • Opaque Type: 不透明类型(some Protocol
  • Protocol-Oriented Programming: 面向协议编程(POP)

术语表

English中文
Protocol协议
Conformance遵守
Protocol Extension协议扩展
Default Implementation默认实现
Associated Type关联类型
Protocol Composition协议组合
Opaque Type不透明类型
Protocol-Oriented Programming面向协议编程
mutating可变(方法会修改 self)
Static Dispatch静态分发
Dynamic Dispatch动态分发

完整示例:Sources/BasicSample/ExampleProtocol.swift


知识检查

问题 1 🟢 (基础概念)

protocol Greetable {
    func greet() -> String
}

struct Person: Greetable {
    let name: String
    func greet() -> String { "Hello, I'm \(name)" }
}

let p: Greetable = Person(name: "Alice")
print(p.greet())

输出是什么?

A) 编译错误 — 协议不能实例化
B) "Hello, I'm Alice"
C) "Greetable"
D) 运行时错误

答案与解析

答案: B) "Hello, I'm Alice"

解析: 协议可以作为类型使用。p 的静态类型是 Greetable,但实际存储的是 Person 实例。调用 greet() 时会动态分发给 Person 的实现。这就是协议多态。

问题 2 🟡 (mutating 关键字)

protocol Flipable {
    var state: Bool { get set }
    func flip()
}

enum Switch: Flipable {
    case on, off
    var state: Bool { self == .on }
    func flip() {
        self = (self == .on) ? .off : .on  // 修改 self
    }
}

能编译通过吗?

A) 能
B) 不能,flip 需要 mutating
C) 不能,枚举不能用 computed property
D) 不能,self 不能赋值

答案与解析

答案: B) 不能,flip 需要 mutating

解析: 枚举是值类型,修改 self(整体赋值)的方法需要标记 mutating。在协议中声明的方法也需要加 mutating

protocol Flipable {
    var state: Bool { get set }
    mutating func flip()  // ✅ 加 mutating
}

enum Switch: Flipable {
    case on, off
    var state: Bool { self == .on }
    mutating func flip() {          // ✅ 结构体/枚举需要
        self = (self == .on) ? .off : .on
    }
}

问题 3 🔴 (协议与 some 的区别)

protocol Drawable {
    func draw() -> String
}

struct Line: Drawable {
    func draw() -> String { "———" }
}

struct Box: Drawable {
    func draw() -> String { "┌─┐\n└─┘" }
}

func createDrawing(useBox: Bool) -> some Drawable {
    if useBox {
        return Box()
    } else {
        return Line()
    }
}

能编译通过吗?

A) 能
B) 不能 — some 要求所有返回路径是同一具体类型
C) 不能 — 协议需要 associatedtype
D) 能 — 会自动装箱

答案与解析

答案: B) 不能 — some 要求所有返回路径是同一具体类型

解析: some Drawable不透明类型,意味着返回值必须是某个具体的、同一的类型,只是隐藏了这个类型给调用者。if/else 返回 BoxLine 两个不同具体类型,违反了此约束。

修复 — 改用协议类型(动态分发):

func createDrawing(useBox: Bool) -> Drawable {  // ✅ 协议类型
    if useBox {
        return Box()
    } else {
        return Line()
    }
}

或者固定返回一种类型:

func createBox() -> some Drawable {   // ✅ 总是返回 Box
    Box()
}

延伸阅读

学习完协议后,你可能还想了解:

选择建议:

💡 记住:Swift 的最佳实践是"协议优先"。能不用继承就不用继承。用协议定义能力,用扩展提供默认实现,用值类型保证安全。


继续学习

  • 下一步:泛型 - 编写可复用的类型安全代码
  • 相关:类与对象 - 理解引用类型的继承体系
  • 进阶:错误处理 - Protocol + 泛型的实战应用

泛型

开篇故事

想象你有一个万能工具箱。这个箱子里有扳手、螺丝刀、钳子,但每个工具都能适配不同尺寸的螺母和螺丝。你不需要为每种尺寸买一套工具,一套就够了。

Swift 中的泛型(Generics)就是这个万能工具。你可以写一段代码,让它适用于多种类型,而不是为每种类型复制一份。当你使用 Array<Int>Array<String>Array<Double> 时,其实底层都是同一份 Array 实现,只是填入了不同的类型参数。

Swift 的标准库几乎完全建立在泛型之上。ArrayDictionaryOptionalResult 都是泛型类型。理解泛型不只是写更少的代码,更是理解 Swift 的类型系统如何做到既灵活又安全。


本章适合谁

如果你已经理解了 Swift 的类型系统和协议基础,想理解如何编写能复用于多种类型的代码,本章适合你。如果你从 Java、C++ 或 Rust 过来,你会发现 Swift 的泛型语法和它们有相似之处,但协议约束系统有自己的独特设计。


你会学到什么

完成本章后,你可以:

  1. 使用 <T> 语法定义泛型函数和泛型类型
  2. 使用类型约束(Type Constraint)限制泛型参数必须符合特定协议
  3. 使用 where 子句对泛型添加多重约束
  4. 理解 Swift 标准库中的泛型设计(Array、Dictionary、Optional、Result)
  5. 在泛型与协议之间做出正确的选择

前置要求

确保你已经阅读了 协议 一章,理解协议定义和协议约束的概念。本章的类型约束会大量用到 EquatableComparableHashable 等协议。


第一个例子

打开 Sources/BasicSample/GenericSample.swift(该文件当前作为泛型相关示例的容器),来看泛型函数最基本的形式:

// 交换两个值的泛型函数
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var x = 10
var y = 20
swapTwoValues(&x, &y)
print("x = \(x), y = \(y)")  // x = 20, y = 10

var name1 = "Alice"
var name2 = "Bob"
swapTwoValues(&name1, &name2)
print("\(name1), \(name2)")  // Bob, Alice

发生了什么?

  • <T> 声明了一个类型参数(Type Parameter),T 是占位符
  • 调用时 Swift 根据实际参数推导出 T 的具体类型
  • inout 允许函数修改传入的参数
  • 同一个函数可以处理 IntString 或任何其他类型

输出

x = 20, y = 10
Bob, Alice

原理解析

1. 泛型函数

泛型函数在函数名后使用 <T> 声明类型参数。T 可以替换为任何类型:

// 检查数组是否包含指定元素
func contains<T: Equatable>(_ array: [T], _ target: T) -> Bool {
    for item in array {
        if item == target {
            return true
        }
    }
    return false
}

print(contains([1, 2, 3, 4, 5], 3))  // true — T = Int
print(contains(["apple", "banana"], "cherry"))  // false — T = String

泛型命名惯例

  • T — 通用的类型参数
  • Element — 集合中的元素
  • KeyValue — 字典的键值对
  • 单字母简短,描述性名称清晰,根据上下文选择

2. 泛型类型

不仅仅是函数,结构体、类、枚举都可以是泛型的:

// 泛型栈 — Stack
struct Stack<Element> {
    private var items: [Element] = []

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        items.popLast()
    }

    func peek() -> Element? {
        items.last
    }

    var isEmpty: Bool {
        items.isEmpty
    }
}

// 使用 — 可以显式指定或让编译器推断
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop() ?? 0)  // 2

var stringStack = Stack<String>()  // T = String
stringStack.push("hello")
stringStack.push("world")
print(stringStack.peek() ?? "empty")  // "world"

泛型枚举

// Swift 标准库的 Optional 就是泛型枚举
enum Maybe<Wrapped> {
    case none
    case some(Wrapped)
}

let number: Maybe<Int> = .some(42)
let nothing: Maybe<String> = .none

3. 类型约束(Type Constraints)

泛型参数可以限制必须遵守某个协议或继承某个类:

// 约束 T 必须遵守 Equatable
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

print(findIndex(of: 9, in: [0, 3, 6, 9, 12]))  // Optional(3)
print(findIndex(of: "hello", in: ["hi", "hello", "hey"]))  // Optional(1)

常见约束协议

协议约束要求用途
Equatable== 运算符相等性检查
Comparable<><=>=排序和比较
Hashablehash(into:)作为字典键
CustomStringConvertibledescription 属性字符串表示
CodableEncodable + DecodableJSON 编解码

4. Where 子句

where 子句允许添加更复杂的约束条件:

// 要求 T 遵守 Equatable,且 U 遵守 Comparable
func compareAndSort<T: Equatable, U: Comparable>(
    _ a: T, _ b: T,
    _ x: U, _ y: U
) -> (T, U) {
    let sorted = (x < y) ? (x, y) : (y, x)
    let equal = (a == b) ? a : b
    return (equal, sorted.0)
}

// 更典型的 where 用法:关联类型约束
func printAll<S: Sequence>(items: S) where S.Element: CustomStringConvertible {
    for item in items {
        print(item.description)
    }
}

printAll(items: [1, 2, 3])           // S.Element = Int
printAll(items: ["a", "b", "c"])    // S.Element = String

where 子句还可以约束关联类型

// 检查两个 Sequence 是否包含相同元素
func sequencesMatch<S1: Sequence, S2: Sequence>(
    _ s1: S1, _ s2: S2
) -> Bool where S1.Element == S2.Element, S1.Element: Equatable {
    let array1 = Array(s1)
    let array2 = Array(s2)
    return array1 == array2
}

print(sequencesMatch([1, 2, 3], [1, 2, 3]))  // true

5. 关联类型在协议中的泛型应用

协议中的 associatedtype 本质上是协议层面的泛型。它与泛型参数不同但互补:

// 用 associatedtype 定义数据源协议
protocol DataSource {
    associatedtype Item
    func fetch() -> [Item]
    var count: Int { get }
}

struct User { let name: String }

struct UserDataSource: DataSource {
    typealias Item = User  // 关联类型的具体化
    var users: [User] = [User(name: "Alice"), User(name: "Bob")]
    func fetch() -> [User] { users }
    var count: Int { users.count }
}

// 用泛型函数使用这个 DataSource
func printDataSource<D: DataSource>(_ source: D)
    where D.Item: CustomStringConvertible {
    for item in source.fetch() {
        print(item)
    }
    print("Total: \(source.count)")
}

泛型参数 vs 关联类型 选择

场景用泛型参数用关联类型
类型由调用者指定
类型由实现者指定
同一类型需要多种实现
函数/类型定义参数
协议能力定义

6. Swift 标准库中的泛型

Swift 标准库几乎全部使用泛型实现。几个最核心的例子:

Array

// Array 的简化定义
struct Array<Element> {
    // 所有操作都基于 Element 类型
}
let numbers: Array<Int> = [1, 2, 3]  // 通常直接写 [Int]

Dictionary

// Dictionary 需要两个类型参数
struct Dictionary<Key: Hashable, Value> {
    // Key 必须遵守 Hashable
}
let dict: Dictionary<String, Int> = ["one": 1, "two": 2]

Optional

// Optional 是最常见的泛型枚举
enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}
let x: Optional<Int> = .some(42)
let y: Int? = 42  // 语法糖,等价于 Optional<Int>

Result

// Result 用于错误处理
enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

func divide(_ a: Double, _ b: Double) -> Result<Double, divisionError> {
    if b == 0 {
        return .failure(DivisionError.byZero)
    }
    return .success(a / b)
}

enum DivisionError: Error { case zero, negative }

7. 泛型 vs 协议 — 什么时候用什么

这是 Swift 开发者最常遇到的选择问题:

// 方案 A: 泛型函数
func process<T: Equatable>(_ value: T) {
    print("Processing: \(value)")
}

// 方案 B: 协议参数
func process(_ value: Equatable) {  // ❌ 不能直接用协议
    print("Processing: \(value)")
}

// 方案 B 的正确写法 — 配合 some
func process(_ value: some Equatable) {
    print("Processing: \(value)")
}

选择指南

需求用泛型用 some Protocol用 Protocol 类型
需要知道具体类型
性能优先
允许多种具体类型混用
编译期类型确定
作为返回类型
返回值可能不同类

经验法则

  • 函数参数优先用 some Protocol(Swift 5.1+)
  • 结构体/类的类型参数用泛型
  • 需要存放混合类型的集合用协议类型(有性能开销)

常见错误

错误 1: 泛型参数没有类型约束

func findItem<T>(_ array: [T], _ target: T) -> Bool {
    for item in array {
        if item == target {  // ❌ T 没有遵守 Equatable
            return true
        }
    }
    return false
}

编译器输出

error: binary operator '==' cannot be applied to two 'T' operands
note: equality operator '==' is declared in protocol 'Equatable'

修复方法

func findItem<T: Equatable>(_ array: [T], _ target: T) -> Bool {
    for item in array {
        if item == target {  // ✅ T 遵守 Equatable
            return true
        }
    }
    return false
}

错误 2: 泛型约束过于严格

func printElements<T: Sequence>(items: T) where T.Element: Comparable {
    // 只为了打印,却要求元素可比较
    for item in items {
        print(item)
    }
}

printElements(items: [1, 2, 3])  // ✅
// printElements(items: [SomeNonComparableType()]) // ❌

修复方法 — 只约束实际需要的:

func printElements<T: Sequence>(items: T) where T.Element: CustomStringConvertible {
    // 只要求能转字符串,更宽松更灵活
    for item in items {
        print(item.description)
    }
}

泛型约束法则:约束应该是满足功能需求的最弱约束。越弱的约束 = 越多的类型可用 = 越好的复用性。

错误 3: 试图用泛型约束两个不相关的参数

func pair<T>(_ a: T, _ b: T) -> (T, T) {
    return (a, b)
}

pair(10, "hello")  // ❌ 两个参数都是 T,必须同一类型

编译器输出

error: cannot convert value of type 'String' to expected argument type 'Int'

修复方法 — 用两个类型参数:

func pair<T, U>(_ a: T, _ b: U) -> (T, U) {
    return (a, b)
}

pair(10, "hello")   // ✅ T=Int, U=String
pair(3.14, true)    // ✅ T=Double, U=Bool

注意:Swift 标准库已经有 Tuple,所以上面的 pair 函数实际上是多余的。这只是用来演示泛型参数的使用。


Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
泛型函数无(运行时类型)fn foo<T>(x: T)func foo<T>(_ x: T)语法高度一致
类型约束无(duck typing)Trait bound T: TraitProtocol constraint T: ProtocolSwift/Rust 约束在编译期
where 子句where T: Traitwhere T: ProtocolSwift/Rust 几乎一致
泛型类型无(运行时)struct Foo<T>struct Foo<T>语法一致
Optional无(用 None)Option<T>Optional<Wrapped> (T?)Swift 用语法糖 ?
Result无(用异常)Result<T, E>Result<Success, Failure>Swift 需要 Error 约束
分发方式运行时单态化(monomorphization)单态化(WITNESS TABLE)Rust/Swift 都没有泛型开销

动手练习

练习 1: 泛型函数

编写一个泛型函数 findMax,接受一个数组,返回最大值。需要添加适当的类型约束:

// 提示:使用 Comparable 协议
func findMax<T>(in array: [T]) -> T? {
    // 你的实现
}
点击查看答案
func findMax<T: Comparable>(in array: [T]) -> T? {
    guard var maximum = array.first else { return nil }
    for item in array.dropFirst() {
        if item > maximum {
            maximum = item
        }
    }
    return maximum
}

print(findMax(in: [3, 1, 4, 1, 5, 9, 2, 6]))  // Optional(9)
print(findMax(in: ["banana", "apple", "cherry"]))  // Optional("cherry")

关键点<T: Comparable> 确保了 > 运算符可用。

练习 2: 泛型类型

实现一个泛型 Result 类型(模拟 Swift 标准库),包含 successfailure 两个 case,以及一个 get() 方法:

enum SimpleResult<Success, Failure: Error> {
    // 你的实现
}
点击查看答案
enum SimpleResult<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)

    func get() -> Result<Success, Failure> {
        switch self {
        case .success(let value):
            return .success(value)
        case .failure(let error):
            return .failure(error)
        }
    }
}

enum MathError: Error {
    case divisionByZero
    case negativeRoot
}

func safeDivide(_ a: Double, _ b: Double) -> SimpleResult<Double, MathError> {
    if b == 0 { return .failure(.divisionByZero) }
    return .success(a / b)
}

let result = safeDivide(10, 3)
switch result {
case .success(let value):
    print("Result: \(value)")
case .failure(let error):
    print("Error: \(error)")
}

练习 3: where 子句

编写一个函数,接受一个字典,打印所有键值对。要求 Key 可转字符串,Value 也可转字符串:

func printDictionary<K, V>(_ dict: [K: V]) where /* 你的约束 */ {
    // 你的实现
}
点击查看答案
func printDictionary<K, V>(_ dict: [K: V])
    where K: CustomStringConvertible, V: CustomStringConvertible {
    for (key, value) in dict {
        print("\(key.description): \(value.description)")
    }
}

printDictionary([1: "apple", 2: "banana", 3: "cherry"])
// 输出(顺序不确定):
// 1: apple
// 2: banana
// 3: cherry

// 也适用于自定义类型
struct User: CustomStringConvertible {
    let name: String
    var description: String { "User(\(name))" }
}
printDictionary(["id": User(name: "Alice")])
// 输出: id: User(Alice)

故障排查 FAQ

Q: 泛型和协议扩展的默认实现有什么区别?

A: 它们解决不同层面问题:

  • 泛型 写一段代码适用于多种类型。比如 swapTwoValues<T>() 可以交换任意两个 T 类型的值
  • 协议扩展 为遵守协议的所有类型提供默认实现。比如为所有 Collection 提供 average() 方法

它们经常配合使用:泛型函数用协议约束参数,协议用关联类型定义抽象能力。

Q: 什么时候用 some Protocol 而不是泛型?

A: 当你是函数作者/类作者时优先用 some Protocol,当你是调用者泛型。原因是:

  • some Protocol 对调用者更简洁 — 他们不需要知道具体的类型参数
  • some Protocol 的返回值隐藏具体类型,提供封装
  • 泛型参数列表在复杂场景下会很冗长,some 更干净
// 泛型版本 — 调用者需要知道或推断 T
func makeValue<T: Shape>() -> T { ... }
let x = makeValue<Circle>()  // 必须指定类型

// some 版本 — 调用者不需要知道类型
func makeDefaultShape() -> some Shape { ... }
let x = makeDefaultShape()  // 编译器推断

Q: Swift 的泛型性能如何?会像 Java 那样有类型擦除的开销吗?

A: Swift 不会类型擦除。Swift 的泛型在编译时会单态化(monomorphization):对每个使用的具体类型,编译器生成一份独立的实例化代码。这意味着:

  • 零运行时开销 — 编译后的代码和手写 Int 版本一样快
  • 编译时间增加 — 更多的泛型实例意味着更多编译工作
  • 二进制增大 — 每个类型实例都有一份副本

这与 Java 的泛型(运行时擦除为 Object)和 Rust 的行为(也是单态化)不同。


小结

核心要点

  1. <T> 是泛型类型参数 — 让函数和类型适用于多种类型
  2. 类型约束 T: Protocol 限制可选类型 — 最常用的约束是 EquatableComparable
  3. where 子句添加多重或关联类型约束 — 泛型的强大表达能力
  4. 标准库大量使用泛型ArrayDictionaryOptionalResult 都是泛型
  5. Swift 泛型是单态化的 — 编译时生成代码,没有运行时开销

关键术语

  • Generics: 泛型(参数化类型)
  • Type Parameter: 类型参数(<T> 中的 T)
  • Type Constraint: 类型约束(T: Equatable
  • Where Clause: where 子句(多重约束)
  • Monomorphization: 单态化(编译时泛型实例化)
  • Associated Type: 关联类型(协议中的占位类型)

术语表

English中文
Generics泛型
Type Parameter类型参数
Type Constraint类型约束
Protocol Conformance协议遵守
Where Clausewhere 子句
Associated Type关联类型
Monomorphization单态化
Type Erasure类型擦除
Generic Type泛型类型
Generic Function泛型函数
Constraint约束

完整示例:Sources/BasicSample/GenericSample.swift


知识检查

问题 1 🟢 (基础概念)

func wrap<T>(_ value: T) -> T {
    return value
}

let x = wrap(42)
let y = wrap("hello")

xy 的类型分别是什么?

A) x: Any, y: Any
B) x: Int, y: String
C) 编译错误 — 不能推断 T
D) x: Int, y: Int

答案与解析

答案: B) x: Int, y: String

解析: Swift 的泛型参数通过调用时的实际参数推断。wrap(42) 时 T 推断为 Intwrap("hello") 时 T 推断为 String。这是 Swift 泛型类型推断的基本能力。

问题 2 🟡 (类型约束)

func sum<T: Numeric>(_ values: [T]) -> T {
    return values.reduce(0, +)
}

Numeric 约束要求 T 必须实现什么?

A) == 运算符
B) + 运算符和数字字面量转换
C) <> 运算符
D) hash(into:) 方法

答案与解析

答案: B) + 运算符和数字字面量转换

解析: Numeric 协议要求类型支持加法运算和整数字面量初始化(init(exactly:))。这确保 reduce(0, +) 中的 0 可以初始化为 T,且 + 可以计算。Equatable 需要 ==Comparable 需要 < >Hashable 需要 hash(into:)

问题 3 🔴 (泛型限制)

struct Pair<T> {
    let first: T
    let second: T
}

func describePairs(_ pairs: [Pair]) {  // ❌
    for p in pairs {
        print("\(p.first), \(p.second)")
    }
}

能编译通过吗?

A) 能
B) 不能 — Pair 需要类型参数
C) 不能 — 数组元素不能是泛型
D) 不能 — describePairs 也需要泛型

答案与解析

答案: B) 不能 — Pair 需要类型参数

解析: Pair 是泛型类型,声明变量或参数时必须指定具体类型参数,或让编译器推断:

// 修复 1:明确指定类型
func describePairs(_ pairs: [Pair<Int>]) {
    for p in pairs {
        print("\(p.first), \(p.second)")
    }
}

// 修复 2:让函数也泛型
func describePairs<T>(_ pairs: [Pair<T>]) {
    for p in pairs {
        print("\(p.first), \(p.second)")
    }
}

// 修复 3:用 some(如果只需要调用 T 的某些接口)
func describePairs(_ pairs: [Pair<some CustomStringConvertible>]) {
    for p in pairs {
        print("\(p.first), \(p.second)")
    }
}

延伸阅读

学习完泛型后,你可能还想了解:

选择建议:

  • 初学者 → 继续学习 错误处理 或回顾协议知识
  • 想深入 Swift 泛型体系 → 阅读 Swift 标准库源码(开源在 GitHub)

💡 记住:泛型是 Swift 类型安全的核心支柱。标准库的每一个集合类型、每一个错误处理机制都以泛型为基础。写泛型代码时,约束应该尽量宽松,满足实际需求即可。


继续学习

  • 回顾:协议 - 理解协议约束是泛型的基础
  • 相关:类与对象 - 泛型也可用于类
  • 进阶:错误处理 - Result<Success, Failure> 是泛型的经典应用

错误处理

开篇故事

想象你在一家餐厅点了一道菜。厨房收到订单后,开始准备。但在做菜的过程中,厨师发现食材用完了,或者火候不对。这时候他需要做两件事。

第一,他必须告诉服务员出了什么问题,让服务员转告客人。第二,如果他已经切了一些菜,必须把工作台清理干净。

Swift 的错误处理机制就是做这两件事的。它让函数能够告诉你"我遇到了一个无法处理的状况",同时确保资源被正确释放。


本章适合谁

如果你写过网络请求、文件读写,或者任何可能失败的操作,你的代码就需要错误处理。本章适合所有 Swift 开发者,无论你是刚开始学编程,还是已经从其他语言转过来了。


你会学到什么

完成本章后,你可以:

  1. 定义遵循 Error 协议 (Error protocol) 的自定义错误类型
  2. 使用 do-try-catch 模式 (do-try-catch pattern) 捕获和处理错误
  3. 理解 throws 关键字 (throws keyword) 的作用范围
  4. 区分try? (optional) 和 try! (force unwrap) 的使用场景
  5. 使用 defer 块 (defer block) 进行资源清理,以及 rethrows 传播错误

前置要求

你需要先掌握 Swift 的枚举 (enum) 和基本函数语法。如果还没学过,请先阅读 基础数据类型函数


第一个例子

打开 Sources/BasicSample/ErrorsSample.swift,让我们从零开始构建一个完整的错误处理示例。

enum FileError: Error {
    case fileNotFound(name: String)
    case permissionDenied
    case diskFull
}

func openFile(_ name: String) throws -> String {
    if name.isEmpty {
        throw FileError.fileNotFound(name: name)
    }
    return "Contents of \(name)"
}

do {
    let content = try openFile("data.txt")
    print(content)
} catch FileError.fileNotFound(let name) {
    print("File '\(name)' does not exist")
} catch {
    print("Unknown error: \(error)")
}

发生了什么?

  • enum FileError: Error — 定义一个错误类型,遵循 Error 协议
  • throws — 标记函数可能抛出错误,调用者必须用 try 处理
  • do { ... } catch — 捕获错误的代码块

输出:

Contents of data.txt

原理解析

1. Error 协议与枚举

Swift 的错误处理核心是 Error 协议。它是一个空协议,任何遵循它的类型都可以作为错误抛出。用枚举表达错误状态是最好的实践:

enum NetworkError: Error {
    case badURL(String)
    case timeout(seconds: Int)
    case noConnection
}

每个 case 可以携带关联值 (associated value),提供额外的上下文信息。比如 timeout(seconds: 30) 能告诉你超时了几秒。

类比

错误就像餐厅菜单上的 "已售罄" 标记。每种情况对应不同的菜品,关联值就是售罄的原因和数量。

2. throws 与 try / do-catch

在函数声明中加上 throws,表示它会抛错:

func divide(_ a: Int, by b: Int) throws -> Double {
    guard b != 0 else {
        throw ArithmeticError.divisionByZero
    }
    return Double(a) / Double(b)
}

调用时必须在 do-catch 块中用 try

do {
    let result = try divide(10, by: 2)  // ✅ 正常
    print("Result: \(result)")
} catch ArithmeticError.divisionByZero {
    print("Cannot divide by zero!")
} catch {
    print("Unexpected error: \(error)")
}

注意 catch 块可以有多个。Swift 会依次匹配,第一个匹配的就执行。最后的 catch 不跟模式,充当兜底。

3. try? vs try!

当错误对你不重要,你只关心结果时,用 try?。它会返回 Optional:

let content = try? openFile("data.txt")
// content 的类型是 String?
// 如果抛错,content 为 nil

try! 告诉编译器"我确定不会报错"。如果真的出错了,程序直接崩溃:

let content = try! openFile("data.txt")
// content 的类型是 String(非 Optional)
// 如果抛错 → 运行时崩溃!

何时用哪个?

  • try? — 结果可以接受为空。比如读取可选配置文件
  • try! — 你 100% 确定不会出错。比如加载打包在 app 里的资源文件

4. 自定义错误与关联值

带关联值的错误能传递更多信息:

enum ValidationError: Error {
    case tooShort(minLength: Int)
    case containsInvalidCharacters(CharacterSet)
    case alreadyUsed(String)
}

func validateUsername(_ name: String) throws {
    if name.count < 3 {
        throw ValidationError.tooShort(minLength: 3)
    }
}

最佳实践:用 localizedDescription 定制用户可读的错误消息:

extension ValidationError: CustomStringConvertible {
    var description: String {
        switch self {
        case .tooShort(let min):
            return "用户名至少需要 \(min) 个字符"
        case .containsInvalidCharacters(let chars):
            return "包含非法字符"
        case .alreadyUsed(let name):
            return "'\(name)' 已经被使用了"
        }
    }
}

5. rethrows — 传播闭包错误

当你的函数接收一个可能抛错的闭包时,用 rethrows 而不是 throws

func mapValues(_ array: [Int], transform: (Int) throws -> Int) rethrows -> [Int] {
    var result: [Int] = []
    for value in array {
        let transformed = try transform(value)
        result.append(transformed)
    }
    return result
}

// 闭包不抛错时,调用不需要 try
let doubled = try mapValues([1, 2, 3]) { $0 * 2 }

// throws 函数也能接收不抛闭包调用
// rethrows 自动适配两种情况

rethrows 的妙处在于,只有当传入的闭包本身会抛错时,调用才需要 try。这比 throws 更灵活。

6. defer — 作用域退出清理

defer 块在当前作用域退出(不管正常退出还是抛错退出)时执行:

func processFile() throws -> String {
    let file = openResource()
    defer {
        closeResource(file)  // 不管成功还是抛错,都会执行
    }
    
    let content = try readFile(file)
    return content  // 作用域退出,defer 先执行
}

多个 defer 从后往前执行 (LIFO):

func multiDefer() {
    defer { print("A") }
    defer { print("B") }
    defer { print("C") }
    // 输出: C, B, A
}

类比:就像餐厅关门前的打扫流程。不管今晚生意好坏,关门时必须清理。defer 就是你的打扫清单。

7. Result 类型 vs throws vs Optional

Swift 的 Result<Success, Failure> 枚举是处理错误的另一种方式:

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

用 Result 作为返回值而不是 throw:

func fetchData(completion: (Result<Data, NetworkError>) -> Void) {
    // 网络请求完成后调用
    if success {
        completion(.success(data))
    } else {
        completion(.failure(.noConnection))
    }
}

什么时候用什么?

  • throws — 同步函数,调用者应该用 do-catch 处理(最常见)
  • Result — 异步回调,因为回调签名无法加 throws
  • Optional — 失败很常见,不需要了解失败原因(比如 JSON 解码)

常见错误

错误 1: 忘记 try

func loadConfig() throws -> String {
    throw ConfigError.missing
}

let config = loadConfig() // ❌ 编译错误!

编译器输出:

error: call can throw but is not marked with 'try'

修复方法:

do {
    let config = try loadConfig() // ✅ 用 try
} catch {
    print("Failed to load config")
}

错误 2: 错误类型没有遵循 Error 协议

enum MyError {  // ❌ 没有遵循 Error
    case somethingBad
}

编译器输出:

error: type 'MyError' does not conform to protocol 'Error'

修复方法:

enum MyError: Error {  // ✅ 加上 Error
    case somethingBad
}

错误 3: 在 throw 之后写代码

func parseValue(_ str: String) throws -> Int {
    guard let value = Int(str) else {
        throw ParseError.invalid
    }
    return value
    print("Parsed!") // ❌ 不可达代码
}

编译器输出:

warning: code after 'throw' will never be executed

修复方法: 删掉 throw 后面的死代码,或者把 print 移到 throw 之前。


Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
错误类型继承 Exception实现 Error trait遵循 Error 协议Swift/Rust 相似
声明可能出错无标记Result<T, E> 返回值throws 关键字Python 无编译时检查
尝试调用try/except手动匹配/? 操作符try + do/catch语法各有特色
直接抛出错误raiseErr(e)? / panic!throwRust 没有 throw 关键字
清理资源finallyDrop trait (RAII)defer语义最接近的是 finally
错误传递异常自动上浮? 操作符rethrowsRust 更简洁

动手练习

练习 1: 定义并抛出错误

定义一个 AgeError 枚举,包含 tooYoung(最小年龄)和 tooOld(最大年龄)两个 case。写一个 validateAge(_ age: Int) throws 函数,年龄小于 0 或大于 150 时抛出对应错误。

点击查看答案
enum AgeError: Error {
    case tooYoung(minAge: Int)
    case tooOld(maxAge: Int)
}

func validateAge(_ age: Int) throws {
    if age < 0 {
        throw AgeError.tooYoung(minAge: 0)
    }
    if age > 150 {
        throw AgeError.tooOld(maxAge: 150)
    }
}

// 测试
do {
    try validateAge(200)
} catch {
    print("Error: \(error)")
}

练习 2: defer 资源管理

写一个函数模拟打开文件和关闭文件。在 defer 中关闭文件,观察正常返回和抛错时 defer 是否都执行。

点击查看答案
enum FileError: Error {
    case emptyContent
}

func processFile() throws -> String {
    print("Opening file...")
    defer {
        print("Closing file...")
    }
    
    let content = ""
    if content.isEmpty {
        throw FileError.emptyContent
    }
    return content
}

// 正常情况
do {
    try processFile()
} catch {
    print("Caught error")
}
// 输出: Opening file... \n Closing file... \n Caught error

// defer 无论抛错还是正常返回都会执行

练习 3: try? 和 try! 的区别

下面的代码分别输出什么?

func mayFail(_ shouldFail: Bool) throws -> String {
    if shouldFail { throw FileError.emptyContent }
    return "OK"
}

let a = try? mayFail(true)
let b = try? mayFail(false)
print("a: \(String(describing: a))")
print("b: \(b!)")
点击查看答案

输出:

a: nil
b: OK

解析:

  • try? mayFail(true) 抛错 → 返回 nil(Optional 包装)
  • try? mayFail(false) 成功 → 返回 Optional("OK")
  • b! 安全解包,因为确实成功了

故障排查 FAQ

Q: try? 和 do-catch 应该怎么选?

A: 看你是否需要区分不同的错误类型:

  • try? — 你只想得到"成功有值"或"失败为 nil",不关心具体原因。比如解析一个可选的配置
  • do-catch — 你需要对不同错误做出不同反应。比如网络请求可能超时、权限不足、服务器错误,每种处理方式都不同

Q: rethrows 和 throws 有什么区别?

A: rethrows 更智能。它只在传入的闭包会抛错时才要求调用者处理错误:

  • throws — 函数一定可能抛错,调用者必须 try
  • rethrows — 函数可能抛出闭包的错误。如果传入的闭包不抛错,调用者不需要 try

标准库中的 map, flatMap 等全是 rethrows

Q: Result 类型什么时候比 throws 更好?

A: Result 主要用在异步回调场景:

func fetchData(completion: (Result<Data, Error>) -> Void)

因为回调函数签名不能加 throws。对于普通的同步函数,直接用 throws + do-catch 更简洁。


小结

核心要点

  1. 错误用枚举表示 — 遵循 Error 协议,case 可携带关联值
  2. throws 标记危险函数 — 调用时必须用 try
  3. do-catch 捕获错误 — 可以匹配特定错误类型,最后用通用 catch 兜底
  4. defer 在作用域退出时执行 — 清理资源,LIFO 顺序
  5. try? 和 try! 是 try 的变体 — try? 返回 Optional,try! 可能崩溃

关键术语

  • Error Protocol: 错误协议(所有错误类型必须遵循)
  • Throw: 抛出(将错误传递出去)
  • Catch: 捕获(处理错误)
  • Rethrows: 传播(传递闭包的错误)
  • Defer: 延迟执行(作用域退出时运行清理代码)

术语表

English中文
Error Protocol错误协议
Throw抛出
Catch捕获
Rethrows传播错误
Defer延迟执行
Associated Value关联值
Force Unwrap强制解包
Result Type结果类型
Try?可选式尝试
CustomStringConvertible自定义字符串转换

完整示例:Sources/BasicSample/ErrorsSample.swift


知识检查

问题 1 🟢 (基础概念)

下面哪个关键字用于标记"可能抛出错误的函数"?

A) throws
B) try
C) catch
D) defer

答案与解析

答案: A) throws

解析: throws 放在函数返回类型箭头 -> 的前面,标记该函数可能抛出错误。调用者必须用 try 配合 do-catch 处理。

问题 2 🟡 (最佳实践)

func process() throws -> String {
    defer { print("A") }
    defer { print("B") }
    defer { print("C") }
    return "Done"
}
let _ = try? process()

输出顺序是什么?

A) A, B, C
B) C, B, A
C) Done, A, B, C
D) Done, C, B, A

答案与解析

答案: B) C, B, A

解析: defer 块按 LIFO(后进先出)顺序执行。最后声明的 defer 最先执行。return 语句先触发 defer 链,然后再真正返回。

问题 3 🟡 (设计决策)

你的异步网络函数需要传递成功或失败结果给回调。应该用哪种方式?

A) throws
B) Result
C) Optional
D) panic

答案与解析

答案: B) Result

解析: 回调函数签名无法用 throwsResult<Data, Error> 枚举是异步场景的标准做法,调用方可以区分成功和失败情况。


延伸阅读

学完错误处理后,你可能还想了解:

选择建议:

  • 初学者 → 继续学习 控制流
  • 有经验开发者 → 跳到 闭包 了解回调中的错误处理

记住:错误处理的核心是"让失败显式化"。Swift 不允许你忽略一个可能出错的函数调用。这是为了你的代码更安全!


继续学习

  • 下一步:闭包 — 理解回调模式和错误传播
  • 相关:并发编程 — async 函数中的错误处理
  • 进阶:协议 — 自定义错误类型的高级技巧

闭包

开篇故事

假设你在一个外卖平台上下了单。你填了地址、选了餐厅、付了款。但在等待的过程中,你并没有闲着。你继续工作、看书、聊天。等外卖到了,平台会通知你。

这就是闭包的精髓。你把一段代码"包好"交给系统,系统在合适的时候执行它。闭包可以记住它在创建时的环境,就像一个外卖订单记住了你的地址和菜品。

Swift 的闭包非常类似 JavaScript 的箭头函数或 Python 的 lambda,但它有更强的类型系统和作用域控制。


本章适合谁

本章适合已经掌握基本函数语法的 Swift 学习者。如果你能写出简单的函数定义,已经了解参数和返回值,就可以开始学闭包。如果你刚从其他语言转过来了,闭包是你一定会遇到的概念。


你会学到什么

完成本章后,你可以:

  1. 使用闭包表达式语法 { (params) -> ReturnType in body }
  2. 理解尾随闭包 (trailing closure) 语法糖
  3. 掌握闭包的值捕获 (value capturing) 机制
  4. 区分逃逸闭包 (@escaping) 和非逃逸闭包
  5. 使用高级函数 map、filter、reduce、sorted、compactMap

前置要求

先完成 函数 章节,了解函数类型 (function type) 和基本参数。本章会多次用到函数类型的概念。


第一个例子

Sources/BasicSample/FunctionSample.swift 中,我们已经看到了嵌套函数和函数返回的案例。这里展示闭包最核心的写法:

// 完整语法
let greet = { (name: String) -> String in
    return "Hello, \(name)!"
}

// 调用闭包
print(greet("Alice"))
// 输出: Hello, Alice!

发生了什么?

  • { (name: String) -> String in ... } — { 包裹的闭包,类型声明紧跟参数,in 分隔签名和函数体
  • name: String — 闭包的输入参数
  • -> String — 闭包的返回类型
  • in — 标记闭包签名结束,函数体开始

原理解析

1. 闭包表达式语法

闭包是"匿名函数"的另一种说法。完整的闭包表达式长这样:

let numbers = [3, 1, 4, 1, 5]

// sorted 需要一个闭包参数
let descending = numbers.sorted(by: { (a: Int, b: Int) -> Bool in
    return a > b
})
print(descending) // [5, 4, 3, 1, 1]

这个闭包告诉 sorted 如何比较两个元素。返回值是 Bool:true 表示 a 排在 b 前面。

类比

闭包就像你给外包团队的说明书。你写清楚"拿到什么数据,返回什么结果",对方按说明执行。

2. 简化写法

Swift 的闭包可以用多种方式简化:

// 1. 参数类型推断(编译器能猜出类型)
let asc1 = numbers.sorted(by: { a, b in a > b })

// 2. 单行表达式自动返回(省略 return)
let asc2 = numbers.sorted(by: { $0 > $1 })

// 3. 尾随闭包 — 闭包是最后一个参数时可以放在括号外面
let asc3 = numbers.sorted { $0 > $1 }

// 4. 如果闭包是唯一参数,括号也可以省略
let asc4 = numbers.sorted(by: >)

一步步来,每个简化都省掉了一些字符。

3. 捕获值 (Capturing)

闭包最大的特色是它能记住创建时的变量:

func makeMultiplier(factor: Int) -> (Int) -> Int {
    { number in
        return number * factor  // 捕获了 factor
    }
}

let double = makeMultiplier(factor: 2)
let triple = makeMultiplier(factor: 3)

print(double(5))  // 10
print(triple(5))  // 15

factormakeMultiplier 返回后并没有消失。它被闭包捕获了,一直存在于内存中。

再看一个更生动的例子:

// functionAsReturnTypeSample() 在 FunctionSample.swift 中
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 10)
print(incrementByTen())  // 10
print(incrementByTen())  // 20
print(incrementByTen())  // 30

runningTotal 在每次调用之间保持状态。这就是捕获的能力:闭包携带了自己的变量,就像一个背包。

4. 尾随闭包 (Trailing Closure)

当闭包是函数的最后一个参数时,Swift 允许把闭包移到括号外:

// 普通写法
let result = numbers.sorted(by: { (a, b) -> Bool in a > b })

// 尾随闭包
let result = numbers.sorted { (a, b) -> Bool in a > b }

如果参数只有闭包,连括号都能省:

let result = numbers.sorted { $0 > $1 }

这是 Swift 中最常见的模式之一。很多 API 都用尾随闭包,比如异步操作、动画、网络请求回调。

5. 逃逸闭包 (@escaping)

有些闭包不会在函数返回前执行,而是"逃到"外面去:

// 非逃逸:闭包在函数内执行完
func processNow(operation: () -> Void) {
    operation()  // 在函数内部执行
}

// 逃逸:闭包保存到变量中
var completionHandlers: [() -> Void] = []

func registerHandler(_ handler: @escaping () -> Void) {
    completionHandlers.append(handler)  // 闭包保存在数组里
}

registerHandler {
    print("This will run later!")
}

@escaping 告诉编译器这个闭包的生命周期比函数调用更长。逃逸闭包需要特别注意循环引用 (retain cycle)。

何时逃逸? 当闭包被保存(赋值给变量、放入数组、传给后台线程)时就需要 @escaping。

6. 自动闭包 (@autoclosure)

@autoclosure 自动把表达式包装成闭包:

var enabled = false

// XCTAssertEqual 内部使用了 @autoclosure
func myAssert(_ condition: @autoclosure () -> Bool, _ message: String) {
    if !enabled { return }  // 闭包根本没执行
    if !condition() {
        print("Assertion failed: \(message)")
    }
}

myAssert(2 + 2 == 5, "Math is broken")
// 因为 enabled 为 false,条件表达式根本没被计算

这叫做"延迟计算"(lazy evaluation)。只有闭包真正被调用时,表达式才会执行。这在单元测试和调试时非常有用。

7. 类型别名 (Type Aliases)

闭包类型写长了很烦人。可以用 typealias 取个名字:

typealias CompletionHandler<T> = (Result<T, Error>) -> Void

func fetchData(completion: CompletionHandler<Data>) {
    // ...
}

greet(person:from:) 这类函数中,返回类型也是函数类型,也可以用 typealias 简化。

8. 高阶函数 (Higher-Order Functions)

Swift 标准库提供了大量接收闭包的工具函数。这些通常叫做"高阶函数",因为它们把函数作为参数。

map — 把一个数组的每个元素转换成另一种:

let words = ["hello", "world", "swift"]
let lengths = words.map { $0.count }
print(lengths)  // [5, 5, 5]

filter — 保留满足条件的元素:

let numbers = [1, 2, 3, 4, 5, 6]
let evens = numbers.filter { $0 % 2 == 0 }
// [2, 4, 6]

reduce — 把所有元素合并成一个:

let sum = numbers.reduce(0, +)  // 21
// 等价于: numbers.reduce(0) { $0 + $1 }

// 拼接字符串
let sentence = words.reduce("") { $0 + " " + $0 }
// 或者用 compactMap 消除 nil

compactMap — 过滤 nil 并同时映射:

let strings = ["1", "abc", "42", "xyz"]
let numbers = strings.compactMap { Int($0) }
// [1, 42] — "abc" 和 "xyz" 转 Int 失败,被过滤掉

sorted — 排序:

let sorted = words.sorted { $0 > $1 }  // 降序
// ["world", "swift", "hello"]

链式组合能力,这些方法返回的还是数组,所以可以连续调用:

let result = numbers
    .filter { $0 % 2 == 0 }
    .map { $0 * $0 }
    .sorted()
print(result)  // [4, 16, 36]

先筛选偶数,再平方,最后排序。每一步都是前一步的结果。


常见错误

错误 1: 尾随闭包语法

let names = ["a", "bb", "ccc"]
let lengths = names.map() { $0.count } // ❌ 编译错误

编译器输出:

error: trailing closure must be passed as the only argument to call

修复方法:

let lengths = names.map { $0.count } // ✅ 去掉 ()

如果闭包不是唯一参数,括号要保留:

let result = reduce(numbers, 0) { $0 + $1 } // ✅ 正确

错误 2: 闭包循环引用

class DataManager {
    var items: [String] = []
    
    func load() {
        fetch { [weak self] data in
            self?.items.append(contentsOf: data)
        }
    }
}

编译器输出:

warning: capturing 'self' strongly in this closure is likely to result in a retention cycle

修复方法:

fetch { [weak self] data in
    self?.items.append(contentsOf: data)
}
// 使用 [weak self] 打破强引用环

错误 3: 逃逸闭包需要标注

var handlers: [() -> Void] = []

func addHandler(_ h: () -> Void) {
    handlers.append(h) // ❌ 不匹配
}

编译器输出:

error: closure is sending non-escaping parameter out of function

修复方法:

func addHandler(_ h: @escaping () -> Void) {
    handlers.append(h) // ✅ 加了 @escaping
}

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
匿名函数lambda`args| { ... }`{ ... in ... }
捕获自动捕获明确模式(move/borrow)自动强捕获Rust 需要 move 关键字
逃逸闭包不需要(引用计数管理)move@escapingSwift 需要标注逃逸闭包
尾随闭包不支持(参数必须是最后一个)不支持支持Swift 独有的语法糖
自动闭包不支持不支持@autoclosureSwift 独有,用于断言等场景
map/filter/reducemap/filter/reduce 函数iter().map()...数组方法 .map{}...Swift 是实例方法

动手练习

练习 1: 用 map 转换数据

给定一个整数数组 [1, 2, 3, 4, 5],用闭包将每个元素平方,再选出大于 4 的结果。

点击查看答案
let numbers = [1, 2, 3, 4, 5]
let result = numbers
    .map { $0 * $0 }
    .filter { $0 > 4 }
print(result)  // [9, 16, 25]

练习 2: 闭包捕获

写一个函数 makeAccumulator() 返回一个闭包,每次调用返回的闭包时,累加传入的值并返回总和。

点击查看答案
func makeAccumulator() -> (Int) -> Int {
    var total = 0
    return { value in
        total += value
        return total
    }
}

let acc = makeAccumulator()
print(acc(10))  // 10
print(acc(20))  // 30
print(acc(5))   // 35

解析: total 被闭包捕获,每次调用都在前一次的基础上累加。

练习 3: reduce 实现字符串拼接

reduce["Hello", " ", "World", "!"] 拼成一个字符串。

点击查看答案
let parts = ["Hello", " ", "World", "!"]
let result = parts.reduce("", +)
print(result)  // Hello World!

// 等价于:
let result2 = parts.reduce("") { $0 + $1 }

故障排查 FAQ

Q: 闭包和函数有什么区别?

A: 概念上几乎相同,区别在于:

  • 函数有名字,用 func 声明,在模块级别定义
  • 闭包是匿名的,用 {} 包裹,通常在函数内部定义
  • 闭包能捕获外部变量,函数不能(函数只接收参数)

实际使用中,当需要一个简短的行为描述时,用闭包。当需要复用、逻辑复杂时,用函数。

Q: 什么时候闭包需要 @escaping?

A: 闭包被保存到函数结束之外的地方时就需要:

// ✅ 非逃逸 — 函数内执行完
func doAndPrint(action: () -> Void) {
    action()
}

// ❌ 编译错误 — 闭包逃出函数
var savedAction: () -> Void

func saveClosure(action: () -> Void) { // 需要 @escaping
    savedAction = action
}

// ✅ 修复:加上 @escaping
func saveClosure(action: @escaping () -> Void) {
    savedAction = action
}

简单记忆:如果闭包被赋值给变量、传入数组、传到后台线程,就加 @escaping。

Q: $0, $1 是什么?

A: 它们是 Swift 闭包的简化用法。当闭包参数名可以省略时,Swift 按位置命名参数:

  • $0 — 第一个参数
  • $1 — 第二个参数
  • $2 — 第三个参数(极少使用)
numbers.filter { $0 > 5 }  // $0 就是每个元素
dict.sorted { $0.key < $1.key }  // $0 和 $1 都是键值对

太长的闭包不要用 $0,可读性差。只用在一两行的简短闭包中。


小结

核心要点

  1. 闭包是匿名函数{ (params) -> ReturnType in body }
  2. 尾随闭包是语法糖 — 最后一个闭包可以移到括号外
  3. 闭包可以捕获值 — 记住创建时的环境,类似于"携带状态的代码块"
  4. 逃逸闭包需标注@escaping 声明生命周期超出函数
  5. 高阶函数组合能力 — 用 map/filter/reduce 链式处理数据

关键术语

  • Closure: 闭包(一段能捕获外部变量的匿名代码块)
  • Trailing Closure: 尾随闭包(将闭包移到函数参数括号之外)
  • Capturing: 捕获(闭包记住并使用外部变量)
  • Escaping Closure: 逃逸闭包(在函数返回后仍然有效的闭包)
  • Higher-Order Function: 高阶函数(接收函数作为参数的函数)

术语表

English中文
Closure闭包
Trailing Closure尾随闭包
Capturing捕获
Escaping逃逸
Autoclosure自动闭包
Type Alias类型别名
Higher-Order Function高阶函数
Retention Cycle循环引用
Shorthand Argument简写参数名
Capture List捕获列表
Lazy Evaluation延迟计算
CompactMap可选映射

完整示例:Sources/BasicSample/FunctionSample.swift(见 functionAsReturnTypeSample)


知识检查

问题 1 🟢 (基础概念)

let names = ["Alice", "Bob", "Charlie"]
let lengths = names.map { $0.count }

lengths 的值是什么?

A) ["Alice", "Bob", "Charlie"]
B) [5, 3, 7]
C) [5, 3, 7] 的字符串形式
D) 编译错误

答案与解析

答案: B) [5, 3, 7]

解析: $0.count 对每个字符串取长度。Alice=5, Bob=3, Charlie=7。返回的是 [Int]

问题 2 🟡 (闭包捕获)

func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}

let c1 = makeCounter()
let c2 = makeCounter()
print(c1())  // A
print(c1())  // B
print(c2())  // C

A, B, C 的值分别是什么?

答案与解析

答案: A=1, B=2, C=1

解析: 每次调用 makeCounter() 都会创建一个新的 count 变量。c1c2 各持有独立的闭包,捕获了各自的环境。

问题 3 🟡 (逃逸闭包)

var handlers: [() -> Void] = []

func add(_ h: @escaping () -> Void) {
    handlers.append(h)
}

为什么需要 @escaping

答案与解析

答案: 因为闭包 h 被添加到了全局数组 handlers 中。它的生命周期超越了 add 函数的调用。

解析: 默认闭包是非逃逸的,只在函数体内有效。一旦闭包被保存到别处(变量、数组、线程),Swift 需要 @escaping 标记来知道这个闭包会存活更久,进而检查循环引用等问题。


延伸阅读

学完闭包后,你可能还想了解:

选择建议:

记住:闭包就是"带状态的代码块"。它能记住创建时的变量,在需要时用。掌握闭包后,你会发现 Swift 的 API 变得异常灵活。


继续学习

  • 下一步:并发编程
  • 相关:控制流 — 闭包在条件表达式中的使用
  • 进阶:协议 — 用闭包替代协议方法的组合模式

并发编程

开篇故事

想象你在一家餐厅的后厨工作。厨师长(主线程)负责摆盘和最终检查。但切菜、炒菜、洗碗这些活不能全让厨师长干。你需要帮手。

在 Swift 过去,我们用 Thread(线程)和 Semaphore(信号量)"人肉管理"这些帮手。就像厨师长站在厨房门口大喊"A去切菜,B去炒菜"。但这有个问题。如果两人都同时去拿同一把刀,就会打架。这叫做"数据竞争"。

Swift 的并发编程是现代化的厨房管理系统。你只需要说"帮我准备这道菜",系统自动分配人手,还能保证不会有人拿到同一把刀。

Sources/BasicSample/ConcurrencySample.swift 中有完整的并发示例。让我们从最基础的 async/await 开始。


本章适合谁

如果你写过网络请求、文件读写,或者任何需要等待外部资源的操作,你就能从并发编程中受益。本章适合有一定 Swift 基础的学习者。如果你是第一次接触 Swift,建议先完成 错误处理闭包


你会学到什么

完成本章后,你可以:

  1. 使用 async/await 语法编写异步代码
  2. 理解 async let 实现并行绑定
  3. 使用 Task 创建和管理后台任务
  4. TaskGroup 实现动态并行计算
  5. 掌握 actorSendable 实现线程安全

前置要求

你需要先理解 Swift 的闭包 (closure)、错误处理 (error handling) 和可选类型 (optional)。如果还没学过,请先阅读 错误处理闭包


第一个例子

打开 Sources/BasicSample/ConcurrencySample.swift,我们从最简单的异步函数开始:

func fetchUser(id: Int) async throws -> String {
    try await Task.sleep(for: .seconds(1))  // 模拟网络延迟
    return "User \(id)"
}

// 调用异步函数需要 await
Task {
    let user = try await fetchUser(id: 1)
    print(user)  // User 1
}

发生了什么?

  • async 标记函数是异步的,意味着它可以在执行过程中暂停
  • await 表示"暂停当前代码,等这个异步操作完成再继续"
  • throws 表示函数可能抛出错误

关键区别:和传统的回调方式不同,代码写起来就像同步代码一样await 暂停后,继续往下执行。没有回调地狱 (callback hell)。


原理解析

1. async/await 基本概念

异步函数的调用看起来就是普通的函数调用:

@available(macOS 13.0, *)
func generateSlideshow(forGallery gallery: String) async throws {
    let photos = try await listPhotos(inGallery: gallery)  // 等待获取照片

    for photo in photos {
        await Task.yield()  // 让出线程,允许其他任务运行
        print("photo:", photo)
    }
}

关键点

  • async 函数只能在另一个 async 函数或 Task 中被调用
  • await 是标记,告诉编译器"这里有暂停的可能"
  • 代码从上到下阅读,就像普通的同步代码

2. async let — 并行绑定

当你需要同时启动多个独立任务时,async let 让绑定是异步的,但后续再 await

async let user1 = fetchUser(id: 1)
async let user2 = fetchUser(id: 2)
async let user3 = fetchUser(id: 3)

do {
    let users = try await [user1, user2, user3]
    print("Fetched users: \(users)")
    // 三个请求是同时发出的,不是等第一个完第二个才开始
} catch {
    print("Error: \(error)")
}

这三个请求是并行的。如果每个请求需要 1 秒,三个并行总共还是 1 秒左右(忽略网络开销)。

如果每个请求顺序执行则需要 3 秒。

async let 的妙处在于:声明后任务立即开始,你可以先做其他事,等真的需要结果时再 await

3. Task — 创建并发任务

Task 是并发编程的基本单元:

// 创建一个新任务
let task = Task {
    print("Running in background")
    let result = try await fetchUser(id: 42)
    return result
}

// 等待任务完成并获取结果
let user = try await task.value

Task.detached 创建的是不继承当前上下文的独立任务:

let detached = Task.detached {
    // 这个任务不继承当前任务的优先级、本地存储等
    print("Independent task")
}

类比

Task 就像你给厨师长派个帮手,帮手继承了厨师长的工作环境。Task.detached 则是另招一个新人,从头开始。

4. TaskGroup — 动态并行

当你不知道需要创建多少任务时(比如处理文件列表中的每个文件),用 TaskGroup:

// 来自 ConcurrencySample.swift 的 BatchCounter 示例
await withTaskGroup(of: Void.self) { group in
    for i in 1...1_000_000 {
        group.addTask {
            await counter.increment()
        }
    }
    // 退出闭包时,自动等待所有任务完成
}
  • withTaskGroup 创建一个任务组
  • addTask 向组中添加任务
  • 闭包结束时自动 await 所有任务完成

这比手动管理 100 万个 Task 方便得多。Swift 底层会帮你调度,充分利用多核 CPU。

5. @MainActor — 主线程隔离(MainActor)

在 iOS/macOS 应用中,更新 UI 必须在主线程上:

@MainActor
func updateUI() {
    // 这个方法保证在主线程执行
    label.text = "Updated!"
    imageView.image = newImage
}

// 在后台任务中调用:
Task {
    let data = try await fetchFromNetwork()
    // 回到主线程更新 UI
    await MainActor.run {
        updateUI()
    }
}

@MainActor 是 Swift 对主线程的标注。被它标注的函数/类只能在主线程调用。编译器会确保这一点,不需要你手动检查。

6. Sendable — Swift 6.0 严格并发

Swift 6.0 引入了 Strict Concurrency(严格并发),核心是 Sendable 协议:

// 不可变数据天然安全
struct Config: Sendable {
    let apiKey: String
    let timeout: Int
}

// 引用类型需要显式标注
actor UserCache: Sendable {
    private var cache: [String: String] = [:]

    func get(_ key: String) -> String? {
        cache[key]
    }
}

Sendable 意味着"这个类型的值可以安全地跨线程传递"。Swift 6.0 编译器会在编译时检查所有跨线程数据传递,从根本上杜绝数据竞争。

传统方式 vs Swift 6.0:

  • 传统方式:运行时崩溃(数据竞争),需要你用锁、信号量等手动避免
  • Swift 6.0:编译时检查,不符合 Sendable 的代码无法通过编译,把数据竞争消灭在编译阶段

7. async/await vs 传统回调 Completion Handler 的写法:

// 传统回调方式
func fetchUser(id: Int, completion: (Result<String, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        // ...
    }
}
// async/await 方式
func fetchUser(id: Int) async throws -> String {
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8)!
}

对比之下,async/await 方式

  • 代码更简洁,没有嵌套的回调
  • 错误处理用 try/catch,更直观
  • 调试友好,断点可以正常设置
  • 避免了回调地狱 (callback hell)

8. AsyncSequence — 异步序列

对于需要持续接收的数据(比如传感器读数、实时消息),用 AsyncSequence

// 来自 ConcurrencySample.swift 的 SensorManager
class SensorManager {
    func startMonitoring() -> AsyncStream<String> {
        let (stream, continuation) = AsyncStream.makeStream(of: String.self)

        Task {
            let dataPoints = ["25°C", "26°C", "27°C"]
            for point in dataPoints {
                try await Task.sleep(for: .seconds(1))
                continuation.yield(point)  // 发送数据
            }
            continuation.finish()  // 结束流
        }

        return stream
    }
}

// 消费端 — for-await 循环
for await value in sensorStream {
    print("Received: \(value)")
}

AsyncSequence 和普通的 Sequence 类似,只是迭代时要用 for await。这非常适合需要流式处理数据的场景。

9. withCheckedContinuation — 桥接旧 API

当你要用旧有的 Completion Handler 包装成 async 函数:

func loadResource(url: URL) async throws -> Data {
    // 旧 API:URLSession.dataTask 使用回调
    try await withCheckedThrowingContinuation { continuation in
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else if let data = data {
                continuation.resume(returning: data)
            }
        }
        task.resume()
    }
}

withCheckedContinuation 把你的回调"桥接"成 async 函数。编译器会检查是否 resume 了,如果忘记调用 resume 会崩溃(开发阶段提醒)。


常见错误

错误 1: 忘记 await

func fetch() async -> String { "data" }

let data = fetch() // ❌ 编译错误!

编译器输出:

error: call to async initializer 'fetch()' in a synchronous function

修复方法:

let data = await fetch() // ✅ 加上 await

错误 2: 在非 async 函数中调用 async 函数

func syncFunction() {
    let data = await fetch() // ❌ 不能在同步函数中使用 await
}

编译器输出:

error: 'async' call in a function that does not support concurrency

修复方法: 把外层函数也标记为 async,或者用 Task 包装:

func syncFunction() {
    Task {
        let data = await fetch()
    }
}

错误 3: 数据竞争(违反 Sendable)

class Counter {
    var count = 0

    func increment() {
        count += 1  // 多线程调用会出问题
    }
}

// Swift 6.0 编译器报错
Task.detached {
    await counter.increment() // 可能报错
}

编译器输出:

error: reference to class 'Counter' is not concurrency-safe

修复方法: 使用 actor 替代 class:

actor Counter {
    var count = 0

    func increment() {
        count += 1  // ✅ 安全,actor 保证串行访问
    }
}

Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
异步函数async defasync fnasync 函数语法几乎一样
等待操作awaitawaitawait完全一致
协程/任务asyncio.Tasktokio::TaskTask都类似
并行绑不支持直接 join TaskGroup`async letPython 需手动
共享数据GIL(锁)Arc<Mutex<T>>Actor + SendableSwift 编译时检查
数据竞争保护所有权系统 + SendableSendable + Strict ConcurrencySwift 6.0 编译时
异步流async forStream traitAsyncSequence都有

动手练习

练习 1: async let 并行加载

写一个函数,同时发起 3 个模拟网络请求(每个延迟 1 秒),总共只需约 1 秒。用 async let 实现。

点击查看答案
func mockFetch(id: Int) async -> String {
    await Task.sleep(for: .seconds(1))
    return "Result \(id)"
}

func runParallel() async {
    async let r1 = mockFetch(id: 1)
    async let r2 = mockFetch(id: 2)
    async let r3 = mockFetch(id: 3)

    let results = await [r1, r2, r3]
    print("All done: \(results)")
}
// 总耗时约 1 秒(并行),而非 3 秒(串行)

练习 2: 用 actor 实现线程安全计数器

写一个 actor 实现计数器,包含 increment()getCount() 方法。创建 10 个并发任务各调用 increment 100 次,最后打印结果。

点击查看答案
actor ThreadSafeCounter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getCount() -> Int {
        value
    }
}

func testCounter() async {
    let counter = ThreadSafeCounter()

    // 10个并发任务,每个加100次
    let tasks = (1...10).map { _ in
        Task {
            for _ in 0..<1000 {
                await counter.increment()
            }
        }
    }

    await withTaskGroup(of: Void.self) { group in
        for task in tasks {
            group.addTask {
                await task.value
            }
        }
    }

    let final = await counter.getCount()
    print("Final count: \(final)")  // 应该是 10000
}

解析: 每个任务的 increment() 是 actor 的方法,actor 保证串行执行,所以即使并发也不会丢失数据。

练习 3: async/await vs Completion Handler 对比

把下面 Completion Handler 风格的函数改写成 async/await:

func oldStyleLoad(name: String, completion: @escaping (Data?) -> Void) {
    DispatchQueue.global().async {
        let url = Bundle.main.url(forResource: name, withExtension: "txt")!
        completion(try? Data(contentsOf: url))
    }
}
点击查看答案
func asyncLoadResource(name: String) async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        DispatchQueue.global().async {
            let url = Bundle.main.url(forResource: name, withExtension: "txt")!
            if let data = try? Data(contentsOf: url) {
                continuation.resume(returning: data)
            } else {
                continuation.resume(throwing: CocoaError(.fileReadCorruptFile))
            }
        }
    }
}

// 调用方
let data = try await asyncLoadResource(name: "test")
print("Loaded \(data.count) bytes")

解析: 用 withCheckedThrowingContinuation 把回调包装成 async 函数,调用方可以直接用 await


故障排查 FAQ

Q: async/await 和 GCD (Grand Central Dispatch) 有什么区别?

A: GCD 是基于线程的,你需要手动管理线程切换;async/await 是基于任务的(Task),系统自动调度:

// GCD 方式
DispatchQueue.global().async {
    let data = fetchData()
    DispatchQueue.main.async {
        updateUI(data)
    }
}

// async/await 方式
func load() async {
    let data = await fetchData()  // 自动返回主线程
    updateUI(data)  // 自动回到主线程
}

async/await 的优点:

  • 更少的线程切换:系统自动选择最优线程
  • 更好的错误处理:用 try/catch
  • 结构化并发:Task 有生命周期管理,不会"忘记"等待
  • 编译时安全:Swift 6.0 强制检查数据竞争

Q: Actor 和 class 有什么区别?

A:

  • class — 线程不安全。多个线程同时访问会导致数据竞争
  • actor — 线程安全。actor 内部的方法每次只能被一个任务调用
class UnsafeCounter {
    var count = 0
    func increment() { count += 1 }  // ❌ 多线程不安全
}

actor SafeCounter {
    var count = 0
    func increment() { count += 1 }  // ✅ 安全
}

简单记忆:需要线程安全时用 actor,其他用 class。

Q: Sendable 是什么?我的类型都需要实现它吗?

A: Sendable 是标记"可以在线程安全地传递的类型。

  • 值类型(struct、enum)— 天然 Sendable(每次传递是拷贝)
  • 引用类型(class、actor)— 需要显式标注 @Sendable 并保证线程安全
  • 只包含不可变数据的 struct — 自动符合 Sendable

Swift 6.0 模式下,跨线程传递非 Sendable 类型会编译报错。


小结

核心要点

  1. async/await 让异步代码可读性高 — 写起来像同步代码,但底层是异步的
  2. async let 实现并行 — 多个独立任务同时启动,最后 await
  3. Task 是并发基本单元 — 后台执行异步操作
  4. TaskGroup 管理大量任务 — 动态创建、自动等待
  5. Swift 6.0 编译时保证安全 — Sendable + actor 消灭数据竞争

关键术语

  • Async/Await: 异步编程关键字(标记异步函数和执行暂停)
  • Task: 并发任务(异步执行的基本单位)
  • Actor: 线程隔离类型(保证串行访问内部状态)
  • Sendable: 可安全跨线程传递(标记线程安全类型)
  • TaskGroup: 任务组(管理多个子任务的创建和等待)
  • AsyncSequence: 异步序列(流式数据传输)
  • Continuation: 延续(桥接回调到 async 的机制)

术语表

English中文
Async/Await异步等待
Task并发任务
TaskGroup任务组
Actor角色(线程安全类型)
@MainActor主线程隔离
Sendable可安全传递
Strict Concurrency严格并发(Swift 6.0)
AsyncSequence异步序列
Continuation延续
Data Race数据竞争
Structured Concurrency结构化并发
Completion Handler完成回调
DispatchSemaphore信号量

完整示例:Sources/BasicSample/ConcurrencySample.swift(asyncTaskSample / actorSample / batchAcotrSample / asyncStreamSample / simpleThreadSample)


知识检查

问题 1 🟢 (基础概念)

async 函数的调用必须在什么环境中?

A) 任何函数中直接调用
B) 另一个 async 函数或 Task 中
C) 主线程中
D) do-catch 块中

答案与解析

答案: B) 另一个 async 函数或 Task 中

解析: 异步函数只能在同样支持并发的上下文中调用。要么在另一个 async,要么用 Task 包装。直接在同步函数中调用 await 会导致编译错误。

问题 2 🟡 (设计决策)

有 10 个 HTTP 请求相互独立。如何最小化总耗时。应该用什么?

A) 顺序调用 10 次
B) 用 async let 同时发出
C) 用 10 个 Thread
D) 用 GCD 的 DispatchQueue

答案与解析

答案: B) 用 async let 同时发出

解析: 所有请求相互独立,用 async let 可以立即全部发出,总耗时至最多请求的时间。Thread/GCD 需要手动管理线程,async let 更简洁。

问题 3 🔴 (actor 隔离)

actor BankAccount {
    private var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount
    }

    func withdraw(_ amount: Double) {
        balance -= amount
    }

    func getBalance() -> Double {
        balance
    }
}

let account = BankAccount()
// 在 Task 中并发调用
await account.deposit(100)
await account.withdraw(30)
let bal = await account.getBalance()
print(bal)

bal 的值是什么?为什么是安全的?

答案与解析

答案: 70.0

解析: actor 内部的方法调用总是串行的,不会并发。所以 deposit 和 withdraw 不会同时执行,不存在数据竞争。await 等待方法完成后再获取结果,确保数据一致性。


延伸阅读

学完并发编程后,你可能还想了解:

选择建议:

  • 初学者 → 继续学习 错误处理闭包
  • 有经验开发者 → 探索 进阶 JSON 处理 中的 SwiftNIO 异步编程
  • 准备生产级应用 → 阅读 Swift 并发安全指南和 Sendable 要求

记住:Swift 的并发设计哲学是"让正确的做法变得简单"。async/await 让异步代码像同步代码一样易读,actor 让线程安全像类一样简单,Swift 6.0 让数据竞争在编译时就消失。这是现代并发编程的未来方向。


继续学习

  • 下一步:错误处理 — 异步操作中的错误处理模式
  • 相关:闭包 — 回调式并发 vs async/await
  • 进阶:SwiftNIO — 高性能网络服务的并发实践

高级进阶 (Advance)

📖 学习内容概览

欢迎完成 Swift 基础部分的学习!高级进阶 部分将带你深入 Swift 的生态系统与工程化实践。从 JSON 处理到异步网络编程,从系统编程到测试框架,这些知识将帮助你编写生产级别的 Swift 代码。


🎯 你将学到什么

完成本部分学习后,你将能够:

  1. 处理 JSON 数据 - 使用 JSONSerialization、JSONDecoder/Encoder 和 SwiftyJSON
  2. 操作文件系统 - 使用 FileManager 进行文件读写、目录遍历、临时文件管理
  3. 持久化数据 - 使用 SwiftData @Model、ModelContainer、ModelActor 构建 CRUD 应用
  4. 管理环境配置 - 使用 swift-dotenv 管理 .env 环境变量
  5. 构建网络服务 - 使用 SwiftNIO 创建 TCP 服务器,理解 EventLoop、Channel
  6. 集成 async/await - 将 SwiftNIO Future 与现代 Swift 并发模型结合
  7. 系统级编程 - 使用 Process 执行命令,处理 Signal,跨平台部署
  8. 编写测试 - 使用 XCTest 编写单元测试、异步测试、性能基准
  9. 构建 Web API - 使用 Vapor 创建 REST API、路由、中间件
  10. 操作 SQLite 数据库 - 使用 GRDB 进行类型安全的 SQL 查询
  11. 并发安全编程 - 使用 Actor 和 Sendable 构建线程安全代码
  12. 高级 Swift 特性 - Property Wrappers、ARC、Opaque Types、Unsafe Pointers、Macros、Result Builders、Mirror Reflection

📚 章节列表

Phase 1: 数据处理与持久化

章节说明难度预计时间
JSON 处理JSONSerialization, JSONDecoder/Codable, SwiftyJSON🟡 中等45 分钟
文件操作FileManager, 临时文件, AsyncLineSequence 流式读取🟡 中等40 分钟
SwiftData 持久化@Model, ModelContainer, ModelActor, #Predicate🔴 困难60 分钟
环境配置ProcessInfo, swift-dotenv, 动态成员查找🟢 简单30 分钟

Phase 2: 网络与系统编程

章节说明难度预计时间
SwiftNIO 网络基础EventLoop, Channel, ByteBuffer, Echo Server🔴 困难50 分钟
SwiftNIO async/awaitFuture 桥接, NIOLoopBoundBox, NIOAsyncChannel🔴 困难40 分钟
系统编程Process 执行, Signal 处理, 跨平台路径🟡 中等35 分钟
测试框架XCTest, async 测试, measure 性能基准🟢 简单25 分钟

Phase 3: Web 与数据库

章节说明难度预计时间
Vapor Web 框架路由, 中间件, Content 协议, REST API🔴 困难45 分钟
GRDB SQL 数据库Record 协议, QueryInterface, 事务, 关联🔴 困难45 分钟

Phase 4: 并发深入

章节说明难度预计时间
Actors 并发模型actor 定义, 隔离域, await 调用🟡 中等30 分钟
Sendable 并发安全Sendable 协议, @Sendable 闭包, 编译器检查🟡 中等30 分钟

Phase 5: Swift 高级特性

章节说明难度预计时间
Property Wrappers@propertyWrapper, wrappedValue, projectedValue🟡 中等25 分钟
ARC 内存管理引用计数, strong/weak/unowned, 捕获列表🟡 中等25 分钟
Opaque/Existential 类型some vs any, 类型擦除, 关联类型🟢 简单20 分钟
Unsafe Pointers指针运算, MemoryLayout, C 互操作🔴 困难30 分钟
Swift Macros@attached, @freestanding, 宏展开🔴 困难30 分钟
Result Builders@resultBuilder, buildBlock, ViewBuilder 原理🟡 中等25 分钟
Mirror ReflectionMirror, children, displayStyle, 反射限制🟢 简单20 分钟

平台要求:

  • SwiftData: macOS 14.0+ (Sonoma)
  • SwiftNIO/Vapor/GRDB: macOS 12.0+ 或 Linux (Ubuntu 22.04+)
  • 文件操作 async APIs: macOS 12.0+
  • Swift Macros: Swift 5.9+ (Xcode 15+)

🔗 前置要求

必须完成:

  • 基础部分所有章节(变量与表达式 → 并发编程)
  • 理解 Swift 的 async/await 语法
  • 理解 do/catch/try 错误处理模式

建议具备:

  • 基本的文件系统和命令行操作经验
  • 了解 JSON 数据结构
  • 了解 TCP/IP 网络基础概念

📈 学习路径

Phase 1 (数据处理):
JSON 处理 → 文件操作 → SwiftData 持久化 → 环境配置

Phase 2 (网络与系统):
SwiftNIO 网络基础 → SwiftNIO async/await → 系统编程 → 测试框架

Phase 3 (Web 与数据库):
Vapor Web 框架 → GRDB SQL 数据库

Phase 4 (并发深入):
Actors 并发模型 → Sendable 并发安全

Phase 5 (Swift 高级特性):
Property Wrappers → ARC 内存管理 → Opaque Types → Unsafe Pointers → Macros → Result Builders → Mirror Reflection

✅ 学习检查点

完成本部分后,你应该能够:

  • 使用 JSONDecoder 和 SwiftyJSON 解析嵌套 JSON 数据
  • 使用 FileManager 创建、读取、删除文件和目录
  • 使用 SwiftData @Model 定义数据模型并执行 CRUD 操作
  • 使用 swift-dotenv 管理 .env 环境变量
  • 使用 SwiftNIO 创建简单的 Echo Server
  • 将 SwiftNIO Future 与 async/await 桥接
  • 使用 Process 执行外部命令并捕获输出
  • 使用 XCTest 编写单元测试和异步测试
  • 使用 Vapor 创建 REST API 和中间件
  • 使用 GRDB 执行类型安全的 SQL 查询
  • 使用 Actor 构建线程安全的共享状态
  • 理解 Sendable 协议并修复并发安全错误
  • 使用 Property Wrappers 封装可复用属性逻辑
  • 使用 weak/unowned 打破 ARC 循环引用
  • 正确使用 some/any 进行类型抽象
  • 使用 UnsafePointer 进行底层内存操作
  • 理解 Swift Macros 的编译时代码生成机制
  • 使用 Result Builders 创建声明式 DSL
  • 使用 Mirror 运行时检查类型结构

🎓 实践项目

建议练习:

  1. 编写一个读取 JSON API 响应并保存到 SwiftData 数据库的应用
  2. 实现一个从 .env 加载配置并写入日志文件的工具
  3. 创建一个 SwiftNIO Echo Server,支持多客户端并发连接
  4. 为你的代码库编写 XCTest 测试覆盖核心逻辑
  5. 实现一个 CLI 工具,调用 git 命令获取仓库信息
  6. 使用 Vapor 构建一个 Todo List REST API,集成 GRDB 持久化
  7. 创建一个 Actor 管理的并发安全缓存系统
  8. 使用 Property Wrappers 实现输入验证框架
  9. 使用 Result Builders 构建简易 HTML 生成 DSL

➡️ 下一步

完成高级进阶后,继续学习 实战精选 部分,你将看到:

  • 第三方库集成示例
  • LeetCode 题目实现
  • 工程化的最佳实践

准备好了吗?让我们开始 JSON 处理 的学习! 🚀

JSON 处理

开篇故事

想象你在一家国际餐厅点菜。菜单用法语写的,你对法语一窍不通。这时你掏出手机,用一个翻译 App 拍照扫描,屏幕上立刻显示出中文翻译。你终于知道自己点的是香煎鳕鱼还是炸薯条了。

JSON 在互联网世界里扮演的就是这个翻译角色。后端用 Python 写,前端用 JavaScript 跑,移动端用 Swift 开发。大家语言不同,但都能看懂 JSON。它就是把数据从"一种语言"翻译成"另一种语言"的那个 Universal Translator。

本章要教你的,就是如何用 Swift 读写这份"世界通用菜单"。

本章适合谁

如果你满足以下任一情况,这一章就是为你准备的:

  • 你需要从网络 API 获取数据,而这些数据的格式是 JSON
  • 你听说过 Codable 但不太确定怎么用
  • 你曾经被 try! 坑过,想要知道更好的写法
  • 你想知道 SwiftyJSON 到底好在哪里,是不是有必要引入

本章面向已经会写基础 Swift 语法的开发者。你不需要是高手,只要知道怎么声明变量、写个函数就行。

你会学到什么

完成本章后,你将掌握以下内容:

  • JSONSerialization(Foundation 原生方法):把 JSON 字符串转成字典和数组
  • JSONDecoder + Codable(类型安全方式):把 JSON 直接映射到 Swift 结构体
  • SwiftyJSON(第三方库方式):用链式语法访问嵌套 JSON,不必提前定义模型
  • CodingKeys(键名映射):当后端返回的字段名和你的 Swift 命名规范不一致时如何处理
  • 常见陷阱:如何避免 try! 导致的崩溃,如何处理可选字段,如何应对键名不匹配

前置要求

在开始之前,请确保你已掌握以下内容:

  • Swift 基础语法:变量声明、函数定义、结构体(Struct)
  • 错误处理:docatchtry 的基本用法
  • 集合类型:理解 Dictionary 和 Array 的区别
  • 可选类型(Optional):知道 ?! 的含义

如果你对这些内容还不太熟悉,建议先回顾基础部分(变量与表达式 → 错误处理),然后再回来。

第一个例子

我们先来看一个最基础的例子。目标很明确:从一个 JSON 字符串里提取出用户的名字和年龄。

这段代码来自 AdvanceSample/Sources/AdvanceSample/AdvanceSample.swift 第 54 到 75 行。

// 1. 定义一个和 JSON 键名匹配的结构体
struct User: Codable {
    let name: String
    let age: Int
}

let jsonContext = "{\"name\":\"John\", \"age\":30}"

// 2. 把 String 转成 Data
if let jsonData = jsonContext.data(using: .utf8) {
    let decoder = JSONDecoder()

    do {
        // 3. 把 Data 解码成 User 结构体
        let user = try decoder.decode(User.self, from: jsonData)

        print("Name: \(user.name)")  // 输出: John
        print("Age: \(user.age)")   // 输出: 30
    } catch {
        print("Error decoding JSON: \(error)")
    }
}

三步搞定:定义模型、转成 Data、调用 JSONDecoder.decode。Swift 通过 Codable 协议自动处理了映射逻辑,你不需要手写解析代码。

原理解析

Swift 提供了三种处理 JSON 的方式,各有优劣。

方式一:JSONSerialization(Foundation 原生)

这是最老派的做法。它把 JSON 字符串解析成一个 [String: Any] 字典。问题在于 Any 类型,你需要手动做类型转换,编译器帮不到你。

let json = try JSONSerialization.jsonObject(
    with: jsonString.data(using: .utf8)!,
    options: .allowFragments
)
// json 是 Any 类型,需要用 as? 做类型转换
if let dict = json as? [String: Any] {
    let name = dict["name"] as? String
}

优点:不需要提前定义任何模型,适合结构不明的 JSON。 缺点:类型不安全,运行时才知道哪里出问题。

方式二:JSONDecoder + Codable(类型安全)

这是 Swift 推荐的主流做法。你定义一个遵守 Codable 的结构体,JSONDecoder 自动帮你做映射。编译器会在编译期就检查字段是否匹配。

struct User: Codable {
    let name: String
    let age: Int
}
let user = try JSONDecoder().decode(User.self, from: jsonData)

优点:编译期检查,类型安全,代码简洁。 缺点:需要为每种 JSON 结构定义对应的模型。

方式三:SwiftyJSON(第三方库)

SwiftyJSON 用链式语法让你直接访问嵌套字段,不需要定义模型。

let result = try JSON(data: jsonData)
let name = result["name"].stringValue
let age = result["age"].intValue

优点:访问嵌套 JSON 时语法非常直观,result["user"]["profile"]["bio"] 这样一路点下去就行。 缺点:引入了一个额外的依赖包,类型检查依然不在编译期。

对比来看:日常开发用 Codable 就够了,后端字段经常变动的场景下 SwiftyJSON 更灵活。

常见错误

以下是最容易踩到的三个坑。

错误一:滥用 try!

AdvanceSample.swift 原文里,第 44 行和第 85 行都用了 try!。这在教程代码里没问题,但在真实项目里是定时炸弹。JSON 格式一旦和预期不符,程序直接崩溃。

// 危险写法
let json = try! JSONSerialization.jsonObject(with: data, options: [])

// 安全写法
do {
    let json = try JSONSerialization.jsonObject(with: data, options: [])
} catch {
    print("解析失败: \(error.localizedDescription)")
}

错误二:忘了定义 CodingKeys

后端返回的字段叫 user_name,你的 Swift 结构体里定义的是 userName。如果不做映射,解码会失败。

struct User: Codable {
    let userName: String

    enum CodingKeys: String, CodingKey {
        case userName = "user_name"
    }
}

错误三:可选字段处理不当

后端某些字段可能不传值。如果你把类型定义为非可选的 String,缺失该字段时解码会报错。

// 后端可能不传 bio 字段
struct User: Codable {
    let name: String
    let bio: String?  // 用可选类型,而不是非可选
}

Swift vs Rust/Python 对比

不同语言都有自己的 JSON 处理方式,放在一起对比会更有感觉:

特性Swift (Codable)Rust (serde)Python (json)
声明方式struct User: Codable#[derive(Serialize, Deserialize)]没有类型声明
类型安全编译期检查编译期检查运行期检查
键名映射CodingKeys 枚举#[serde(rename = "...")]手动用字典键访问
可选字段let bio: String?Option<String>永远需要 get()in 判断
嵌套访问需要嵌套模型或 SwiftyJSONuser.profile.biodata["user"]["profile"]["bio"]
错误处理do/catchResult<T, E>try/except

Swift 的 Codable 和 Rust 的 serde 在思路上非常相似,都是通过派生(derive)或协议遵守(conform)来自动生成序列化代码。Python 的做法更灵活但更脆弱,所有检查都推迟到了运行期。

动手练习 Level 1

目标:用 JSONDecoder 解析一个简单的 JSON 对象。

假设你收到这样一段 JSON,里面是一本书的信息:

{"title": "Swift 编程指南", "year": 2024, "author": "李白"}

你的任务是:

  1. 定义一个 Book 结构体,遵守 Codable
  2. 声明三个属性:titleyearauthor
  3. JSONDecoder 把上面的 JSON 解析成 Book 实例
  4. 在控制台打印书名和作者
点击查看答案
struct Book: Codable {
    let title: String
    let year: Int
    let author: String
}

let json = """
{"title": "Swift 编程指南", "year": 2024, "author": "李白"}
"""

if let data = json.data(using: .utf8) {
    let book = try JSONDecoder().decode(Book.self, from: data)
    print("书名: \(book.title), 作者: \(book.author)")
}

动手练习 Level 2

目标:解析带嵌套结构的 JSON,并用 CodingKeys 处理命名不一致。

假设后端返回的 JSON 是这样的:

{
    "user_name": "张三",
    "user_age": 28,
    "profile_pic": "https://example.com/photo.jpg"
}

但你想在 Swift 里使用驼峰命名(userNameuserAgeprofilePic),怎么做?

点击查看答案
struct UserProfile: Codable {
    let userName: String
    let userAge: Int
    let profilePic: String?

    enum CodingKeys: String, CodingKey {
        case userName = "user_name"
        case userAge = "user_age"
        case profilePic = "profile_pic"
    }
}

profilePic 定义为可选类型,因为有些用户可能没有设置头像,后端不会返回这个字段。

动手练习 Level 3

目标:使用 SwiftyJSON 动态访问一个多级嵌套的 JSON。

现在后端返回的数据比较复杂:

{
    "company": "Acme",
    "employees": [
        {
            "name": "Alice",
            "skills": ["Swift", "Rust"]
        },
        {
            "name": "Bob",
            "skills": ["Python"]
        }
    ]
}

用 SwiftyJSON 提取第一个员工的第二个技能(结果是 "Rust")。

点击查看答案
let json = try JSON(data: jsonData)
// 链式访问:先取 employees 数组,再取索引 0 的对象,再取 skills 数组的索引 1
let skill = json["employees"][0]["skills"][1].stringValue
print(skill)  // 输出: Rust

SwiftyJSON 的好处是,即使某个路径不存在,它只会返回空值而不会崩溃。这也是 SwiftyJSON 最大的卖点。

故障排查 FAQ

Q1:解码时报 keyNotFound 错误怎么办?

这说明 JSON 里的某个字段在你的模型中是非可选类型,但 JSON 里缺失了这个键。把该字段改成可选类型(加 ?)或者在 JSON 中补充缺失的字段即可。

Q2:解码时报 typeMismatch 错误怎么办?

JSON 里的值和 Swift 类型对不上。比如后端返回 "age": "30"(字符串),而你的模型定义 let age: Int。检查实际 JSON 数据的类型,或在模型中使用 String

Q3:try! 导致程序崩溃,怎么快速修复?

try! 改成 do { try ... } catch { ... },包裹在 error handling 块内,让错误有机会被捕获。

Q4:后端返回的字段名和我的 Swift 规范不一致怎么办?

使用 CodingKeys 枚举做映射。枚举名用你的 Swift 命名,raw value 用后端的字段名。

Q5:一段 JSON 不确定结构,该用哪种方式?

先试 JSONSerialization,它返回 [String: Any],你可以先用 print 查看结构,然后再决定要不要定义正式的 Codable 模型。

Q6:SwiftyJSON 和 Codable 能混用吗?

不太建议。SwiftyJSON 的设计思想是"不定义模型直接用",Codable 的设计思想是"提前定义模型"。混用会让代码意图混乱。在同一个功能里选一种方式即可。

小结

  • JSON 是跨语言的事实标准,Swift 提供了三种方式来处理它
  • JSONSerialization 返回任意类型,最灵活但不安全
  • JSONDecoder + Codable 是推荐方式,类型安全,编译期检查
  • SwiftyJSON 擅长处理嵌套结构,用链式语法直接访问深层字段
  • 永远不要用 try! 处理不可信的外部数据,用 do/catch 包裹

术语表

英文中文说明
Codable可编解码Swift 协议,声明后可自动实现 JSON 的编码和解码
CodingKeys键名枚举用于映射 Swift 属性名和 JSON 字段名的枚举类型
JSONSerializationJSON 序列化器Foundation 框架提供的旧式 JSON 解析类
JSONDecoderJSON 解码器将 JSON Data 解码为 Swift 类型的类
JSONEncoderJSON 编码器将 Swift 类型编码为 JSON Data 的类
SwiftyJSON第三方 JSON 库提供链式访问语法的第三方处理库

知识检查

用三个问题检验你是否真正掌握了本节内容。

问题一:Codable 实际上是哪两个协议的组合?

查看答案

Codable = Decodable + Encodable。Decodable 负责 JSON 到 Swift 对象的解码,Encodable 负责 Swift 对象到 JSON 的编码。

问题二:如果一个 Swift 属性名叫 createdAt,但 JSON 字段名叫 created_at,应该如何配置 CodingKeys?

查看答案
enum CodingKeys: String, CodingKey {
    case createdAt = "created_at"
}

CodingKeys 作为 CodingKey 协议的枚举,用 raw value 指定 JSON 中的实际字段名。

问题三:SwiftyJSON 的 .stringValue.value 有什么区别?

查看答案

stringValue 返回确定类型(String),如果实际类型不匹配则返回空字符串。.value 返回原始类型(Any),需要你手动做类型转换。日常开发优先用 .stringValue.intValue 等类型化 accessor。

继续学习

JSON 处理完成后,你的数据已经从网络层面落入了 Swift 的世界。下一步,你需要知道如何把这些数据保存下来。

继续学习下一节:文件操作,你将学会如何用 FileManager 读写文件系统,以及 SwiftData 的持久化机制。

文件操作

开篇故事

想象你在一座大型图书馆里工作。图书馆有不同的区域,每种区域存放不同类型的书籍。有的区域存放珍本古籍,需要恒温恒湿。有的区域存放普通阅览书籍。还有的区域专门放读者暂借的书籍,还完就清空。

文件系统在计算机里的角色就像这座图书馆。操作系统帮你管理不同的目录,每个目录有不同的用途。有的目录备份到云,有的目录磁盘满了会清空,还有的目录应用卸载时就一起消失。

Swift 提供了 FileManager(文件管理器)来帮你和这座图书馆打交道。


本章适合谁

如果你需要保存数据到磁盘,或者从磁盘读取数据,就需要文件操作。具体场景包括:

  • 缓存网络请求的图片或 JSON 数据
  • 保存用户设置和偏好
  • 写入日志文件供后续分析
  • 处理大文件时逐行读取,避免内存爆炸

本章适合所有需要持久化数据的 Swift 开发者。无论你是写 macOS 命令行工具还是 iOS 应用,文件操作都是基础能力。


你会学到什么

完成本章后,你可以:

  1. 使用 FileManager 获取 Documents、Caches、Temp、ApplicationSupport 等系统路径
  2. 理解 TemporaryFile 类的 RAII 自动清理模式
  3. 使用 AsyncLineSequence 异步逐行读取大文件
  4. 识别路径不存在、权限不足、跨平台差异等常见错误
  5. 将 Swift 的文件操作与 Rust、Python 做对比

前置要求

你需要先掌握:

  • Swift 基础语法(变量、函数、枚举)
  • do/catch/try 错误处理模式
  • async/await 基础概念

如果还不会,请先学习 基础数据类型错误处理并发编程

平台要求AsyncLineSequence(异步流式读取)需要 macOS 12.0+。其余 API 在所有版本的 macOS 和 iOS 上都可用。


第一个例子

打开 AdvanceSample/Sources/AdvanceSample/FileOperationSample.swift。我们从一个完整的文件管理器路径示例开始。

import Foundation

public func fileManagerPathSample() {
    let fileManager = FileManager.default

    // 1. 获取 Document 目录(用户文档,iCloud 会备份)
    if let documentsURL = fileManager.urls(
        for: .documentDirectory,
        in: .userDomainMask
    ).first {
        print("Document 目录: \(documentsURL.path)")
    }

    // 2. 获取Library/Caches 目录(临时缓存,空间不足时可能清理)
    if let cacheURL = fileManager.urls(
        for: .cachesDirectory,
        in: .userDomainMask
    ).first {
        print("Cache 目录: \(cacheURL.path)")
    }

    // 3. 获取Library/Application Support 目录(存放配置、数据库)
    if let appSupportURL = fileManager.urls(
        for: .applicationSupportDirectory,
        in: .userDomainMask
    ).first {
        print("Application Support 目录: \(appSupportURL.path)")
    }

    // 4. 获取 Temporary 目录(完全临时,应用退出后可能消失)
    let tempPath = NSTemporaryDirectory()
    print("Temp 目录: \(tempPath)")
}

发生了什么?

  • FileManager.default 获取共享的文件管理器实例
  • urls(for:in:) 返回一个 URL 数组,first 取第一个路径
  • 不同目录用途各异,选择合适的目录能让系统更好管理存储空间

输出(路径因机器而异):

Document 目录: /Users/username/Library/Containers/.../Data/Documents
Cache 目录: /Users/username/Library/Containers/.../Library/Caches
Application Support 目录: /Users/username/Library/Containers/.../Data/Application Support
Temp 目录: /private/var/folders/.../T/

原理解析

1. 四大系统目录

macOS/iOS 的文件系统有四个核心目录,每个用途不同:

目录英文用途备份清理时机
Documents.documentDirectory用户可见的重要文件iCloud 备份用户/App 卸载
Caches.cachesDirectory缓存数据,可重新下载的网络请求结果不备份磁盘空间不足时
TemporaryNSTemporaryDirectory()完全临时文件,用完即删不备份应用退出后
Application Support.applicationSupportDirectory数据库、配置文件iCloud 备份App 卸载

选择原则:用户生成文件放 Documents,缓存放 Caches,数据库放 Application Support。

2. TemporaryFile 与 RAII 自动清理

public class TemporaryFile {
    public let url: URL

    public init(content: String) throws {
        self.url = FileManager.default.temporaryDirectory
            .appendingPathComponent("temp-\(UUID().uuidString).txt")
        try content.data(using: .utf8)?.write(to: self.url)
    }

    deinit {
        if FileManager.default.fileExists(atPath: url.path) {
            try? FileManager.default.removeItem(at: url)
        }
    }
}

deinit 是 Swift 的析构函数。当对象不再被引用、内存被回收时,deinit 自动执行。这种模式在 Rust 里叫 RAII(Resource Acquisition Is Initialization),确保临时文件不会泄露。

类比:就像图书馆的"暂借书架"。读者还书后,工作人员自动把书放回原处。你不需要手动记得放回去。

3. AsyncLineSequence 流式读取

public func readLines() async throws -> AsyncLineSequence<URL.AsyncBytes> {
    return url.lines
}

// 使用
for try await line in try await temp.readLines() {
    print("读取到: \(line)")
}

url.lines 创建一个异步序列,逐行加载文件。每次只读一行到内存,适合处理 GB 级别的日志文件。


常见错误

错误 1: 路径不存在

let url = URL(fileURLWithPath: "/nonexistent/data.txt")
let content = try String(contentsOf: url) // ❌ 运行时异常

修复方法:先检查文件是否存在:

guard FileManager.default.fileExists(atPath: url.path) else {
    print("文件不存在,跳过读取")
    return
}

错误 2: 权限不足

尝试写入 Bundle 内部目录。Bundle 是只读的(只读文件系统),应用只能写 Documents、Caches、Temporary 等沙盒目录:

// ❌ 错误示例:写入 Bundle 资源目录
let bundlePath = Bundle.main.resourcePath

修复方法:始终写入沙盒目录(Documents、Caches 等)。iOS 的沙盒机制(sandbox)禁止写入应用包内部。

错误 3: Linux 平台差异

FileManager.documentDirectory.cachesDirectory.applicationSupportDirectory 在 Linux 上不可用。Linux 没有 macOS 的 Documents/Caches 目录结构:

// 这段代码在 Linux 上返回空数组:
let docs = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
// docs 是 [] —— 没有匹配结果

解决方法:在 Linux 上使用标准的 POSIX 路径,可以用环境变量或 NSFileManager.default.homeDirectoryForCurrentUser

#if os(Linux)
let homeDir = fileManager.homeDirectoryForCurrentUser.path
#else
let docsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first
#endif

跨平台提示:如果你的代码需要同时跑在 macOS 和 Linux 上,用 #if os() 条件编译来区分。


Swift vs Rust/Python 对比

概念PythonRustSwift关键差异
文件系统接口os.path / pathlibstd::fsFileManagerPython 路径是字符串,Swift 用 URL
获取 Home 目录os.path.expanduser("~")home_dir()homeDirectoryForCurrentUser各有封装
创建临时文件tempfile.NamedTemporaryFile()无标准库方案NSTemporaryDirectory() + UUIDPython 最方便
读取文件内容pathlib.Path.read_text()std::fs::read_to_string()String(contentsOf:encoding:)Rust 需手动打开
文件不存在处理FileNotFoundErrorResult<T, io::Error>throws + do/catchRust 需要手动解 Result
自动清理资源with 语句(上下文管理器)Drop trait(RAII)deinit(引用计数)Rust 编译期保证,Swift 运行期
逐行读取大文件for line in f:BufReader::lines()url.lines asyncSwift 需要 macOS 12+

一句话总结:Python 最简洁,Rust 最安全(编译期保证),Swift 在两者之间平衡。


动手练习

练习 1: 获取 Documents 目录路径

写一个函数 getDocumentsPath(),返回 String?。使用 FileManager 获取 Documents 目录路径。如果获取失败,返回 nil

点击查看答案
func getDocumentsPath() -> String? {
    let fileManager = FileManager.default
    if let url = fileManager.urls(
        for: .documentDirectory,
        in: .userDomainMask
    ).first {
        return url.path
    }
    return nil
}

// 测试
if let path = getDocumentsPath() {
    print("Documents 路径: \(path)")
}

练习 2: 创建并读取临时文件

使用 TemporaryFile 类创建一个临时文件,写入 "Hello Swift File!",然后读取并打印内容。观察程序退出时是否自动清理。

点击查看答案
do {
    let tempFile = try TemporaryFile(content: "Hello Swift File!")
    // 读取内容
    let content = try tempFile.readContent()
    print("文件内容: \(content)")
}
// out of scope → tempFile 引用计数归零 → deinit → 文件删除

练习 3: 异步逐行读取大文件

使用 AsyncLineSequence 逐行读取临时文件内容。创建一个有 3 行内容的临时文件,用 async for-in 循环逐行打印。提示:参考 temporaryFileSample() 函数的第三部分。

点击查看答案(隐藏代码)
func streamFileSample() async throws {
    let temp = try TemporaryFile(content:
        "第一行\n第二行\n第三行"
    )
    for try await line in try await temp.readLines() {
        print("行: \(line)")
    }
    // 退出作用域后临时文件自动删除
}

故障排查 FAQ

Q1: 为什么 urls(for: .documentDirectory, in: .userDomainMask) 返回空数组?

A: 在某些命令行工具或沙盒受限环境中,Documents 目录可能不存在。确保你的应用有正确的沙盒权限,或者尝试使用 .applicationSupportDirectory

Q2: 临时文件没有被自动删除怎么办?

A: deinit 只在对象的引用计数归零时触发。如果你用 var ref = TemporaryFile(...) 保存了引用,确保让引用离开作用域或设置为 nil

var ref: TemporaryFile? = try TemporaryFile(content: "test")
ref = nil // 释放引用 → 触发 deinit → 删除文件

Q3: Linux 上运行报"Document directory not available"怎么办?

A: Linux 没有 macOS 的沙盒目录结构。使用 #if os(Linux) 条件编译,在 Linux 上改用标准的 /tmp/$HOME 路径。

Q4: 读取文件时报 "The file doesn't exist"。怎么办?

A: 检查三件事:路径是否正确拼写、文件是否真的存在(用 fileManager.fileExists(atPath:) 验证)、当前进程是否有读取权限。

Q5: 为什么写入文件时得到 "You don't have permission"?

A: 你尝试写入没有权限的目录(比如 /Applications或应用 Bundle)。Swift 应用只能写入沙盒内的 Documents、Caches、Temporary等目录。


小结

核心要点

  1. FileManager 有四大核心目录 — Documents 存用户文件,Caches 存缓存,Temporary 存临时文件,Application Support 存配置和数据库
  2. 临时文件用 RAII 自动清理 — 利用 deinit 确保不泄露,无需手动删除
  3. 大文件用 AsyncLineSequence 逐行读取 — 需要 macOS 12+,避免内存占满
  4. 权限和路径是两类常见错误 — 先检查文件存在再读写,只写沙盒目录
  5. Linux 平台差异要注意 — 没有 Documents/Caches,用条件编译或 POSIX 路径

术语表

English中文说明
FileManager文件管理器macOS/iOS 提供的文件系统操作类
URL (file URL)文件 URLfile:// 开头的统一资源定位符,比字符串路径更安全
AsyncLineSequence异步行序列macOS 12+ 引入的逐行异步读取类型,适合大文件
RAII资源获取即初始化资源绑定到对象生命周期,对象释放时资源自动清理
deinit析构函数对象被销毁时自动调用的方法
Temporary Directory临时目录应用退出后可能清理的临时文件存储区
Sandbox沙盒iOS/macOS 的安全隔离机制,限制应用可访问的文件范围
UUID通用唯一识别码用于生成唯一的临时文件名

知识检查

问题 1 🟢(基础概念)

以下哪个目录用于存放用户生成的文档且会被 iCloud 备份?

A) Caches
B) Temporary
C) Documents
D) Application Support

答案与解析

答案: C) Documents

解析: .documentDirectory(Documents 目录)存放用户生成的重要文件,会被 iCloud 备份。Caches 不备份,Temporary 最不稳定,Application Support 存配置而非用户文档。

问题 2 🟡(最佳实践)

TemporaryFile 对象离开作用域后自动删除文件,依靠的是什么机制?

A) ARC 引用计数归零后触发 deinit
B) 系统定时器定期扫描
C) 编译器自动插入删除代码
D) deasync 异步清理

答案与解析

答案: A) ARC 引用计数归零后触发 deinit

解析: Swift 使用自动引用计数(ARC)。当 TemporaryFile 的所有引用都消失时,ARC 将其内存回收,并调用 deinit 方法执行清理。

问题 3 🔴(跨平台)

以下代码在 Linux 上运行会有什么结果?

let docs = FileManager.default.urls(
    for: .documentDirectory, in: .userDomainMask
)
print(docs.count)

A) 打印 1
B) 崩溃
C) 打印 0
D) 编译错误

答案与解析

答案: C) 打印 0

解析: Linux 没有 macOS 的 Documents 目录结构。urls(for:in:) 无匹配结果,返回空数组。Linux 上应该用标准的 POSIX 路径,例如 homeDirectoryForCurrentUser


继续学习

  • 下一步:SwiftData 持久化 — 用 Model、ModelContainer 和 #Predicate 管理结构化数据
  • 相关:环境配置 — 使用 swift-dotenv 管理环境变量和配置文件
  • 进阶:并发编程 — 后台线程中安全地读写文件

记住:文件操作的核心原则是"选对目录、处理错误、及时清理"。选对目录,系统会帮你管理空间。忽略错误,用户的文件就会丢。

SwiftData 持久化

开篇故事

想象你运营一家图书馆。读者不断借书还书,你需要记录每本书的流向。如果没有账本,几天后你根本不知道哪些书还在架子上,哪些被借走了。

SwiftData 就像一位非常勤快的图书管理员。你只需要告诉他每本书叫什么名字、哪个类别,他会在背后自动建好索引、管好账本。你只需要用简单的 Swift 类描述数据,SwiftData 就帮你搞定存储、查询、排序所有这些繁琐的事。

你不需要写 SQL,不需要管表结构。你定义一个 class,加上一个 @Model 标签,剩下的事 SwiftData 全包了。


本章适合谁

你正在或用 Swift 开发需要本地持久化数据的应用。你不想手写 SQL,也不想用 CoreData 繁琐的 .xcdatamodel 文件配置,希望用纯代码的方式管理本地数据库。

如果你熟悉 UserDefaults 但发现它存不了复杂对象,或者你已经用 FileManager 存 JSON 文件但遇到了性能瓶颈,SwiftData 就是为你准备的。


你会学到什么

完成本章后,你可以:

  1. 使用 @Model 宏 (Macro) 声明可持久化的数据类
  2. 配置 ModelContainer 管理 SQLite 存储路径和选项
  3. 使用 ModelContext 执行增删改查 (CRUD) 操作
  4. FetchDescriptorSortDescriptor 排序并取回数据
  5. #Predicate 宏写出类型安全的过滤条件
  6. @ModelActor 实现并发安全的后台数据导入
  7. @Relationship 管理一对多、多对多关联和级联删除

前置要求

  • 掌握 Swift 基础语法,尤其是类 (class) 和属性 (property)
  • 理解 async/await 异步编程模型
  • 理解 do/try/catch 错误处理模式
  • macOS 14.0+ (Sonoma) 是硬性要求。SwiftData 是 Apple 在 iOS 17 / macOS 14 引入的 API,低版本系统不可用

⚠️ 重要提醒: 本章所有代码需要 macOS 14.0 或更高版本。在 Package.swift 中需要设置 platforms: [.macOS(.v14)],在代码中需要加 @available(macOS 14, *) 标注。


第一个例子

打开代码文件 AdvanceSample/Sources/AdvanceSample/SwiftDataSample.swift 第 12-26 行,这是 SwiftData 最基础的模型定义:

@available(macOS 14, *)
@Model
final class ServerLog {
    var id: UUID
    var timestamp: Date
    var endpoint: String
    var responseCode: Int

    init(endpoint: String, responseCode: Int) {
        self.id = UUID()
        self.timestamp = Date()
        self.endpoint = endpoint
        self.responseCode = responseCode
    }
}

对比普通 Swift 类,你只做了一件事:加上 @Model。SwiftData 编译器插件会自动把这个类转换成可持久化的数据模型。每个属性都会自动映射到 SQLite 底层的列。

接下来创建容器和上下文,把数据存进去:

let config = ModelConfiguration(
    url: databaseURL,
    cloudKitDatabase: .none
)
let container = try ModelContainer(
    for: ServerLog.self,
    configurations: config
)
let context = ModelContext(container)

let newLog = ServerLog(endpoint: "/index", code: 200)
context.insert(newLog)
try await context.save()

运行结果:

🚀 正在初始化临时数据库:/var/folders/.../server_logs_XXXX.sqlite
log request XXXX-XXXX-XXXX
fetch count: 3
log: XXXX, 2025-12-20 10:30:00, /index, 200
log: XXXX, 2025-12-20 10:30:01, /status, 404
log: XXXX, 2025-12-20 10:30:02, /home/list, 200

原理解析

1. @Model 宏与属性映射

@Model 是 Swift 5.9 引入的宏 (Macro)。编译器会自动为标记的 final class 生成底层持久化支持代码。

支持什么属性类型?

@Model
final class Item {
    var id: UUID           // ✅ 支持
    var name: String       // ✅ 支持
    var count: Int         // ✅ 支持
    var price: Double      // ✅ 支持
    var isActive: Bool     // ✅ 支持
    var createdAt: Date    // ✅ 支持
    var data: Data         // ✅ 支持 (BLOB)
    var tags: [String]     // ✅ 支持 (需要 Transformable)
    var status: ItemStatus // ✅ 支持 (RawRepresentable enum)
}

不支持的类型会报编译错误:

@Model
final class BadModel {
    var closure: () -> Void     // ❌ 编译错误:不支持函数类型
    var anyValue: Any           // ❌ 编译错误:不支持 Any
    var url: URL                // ❌ 编译错误:需要自行转换
}

每个非可选属性都必须是可持久化的。如果属性是可选类型 String?,对应数据库列允许为 NULL。

2. ModelContainer 配置

ModelContainer 是 SwiftData 的核心,它负责:

  • 管理底层 SQLite 文件连接(或内存数据库)
  • 加载数据模型 schema
  • 自动创建表结构

本地文件存储:

let dbURL = FileManager.default.temporaryDirectory
    .appendingPathComponent("app.sqlite")

let config = ModelConfiguration(
    url: dbURL,                        // 文件路径
    cloudKitDatabase: .none,           // 不使用 iCloud
    allowsSaveForward: true            // 允许 schema 向前兼容
)

let container = try ModelContainer(
    for: ServerLog.self,               // 注册模型类型
    configurations: config
)

内存数据库(测试用):

let config = ModelConfiguration(
    isStoredInMemoryOnly: true         // 不落盘,退出即丢失
)
let container = try ModelContainer(
    for: ServerLog.self,
    configurations: config
)

3. ModelContext CRUD 操作

ModelContext 是操作数据库的"工作区",类似 ORM 的 Session。它提供了完整的增删改查:

创建 (Insert):

let newLog = ServerLog(endpoint: "/api/data", responseCode: 201)
context.insert(newLog)
try context.save()  // 同步保存
// 或
try await context.save()  // 异步保存

读取 (Fetch):

let descriptor = FetchDescriptor<ServerLog>()
let allLogs = try context.fetch(descriptor)

更新 (Update):

SwiftData 直接修改对象的属性,然后在 context 中 save 即可:

if let log = allLogs.first {
    log.responseCode = 500          // 直接修改
    try context.save()               // 自动追踪变更
}

删除 (Delete):

// 删除单个对象
context.delete(log)
try context.save()

// 批量删除
try context.delete(
    model: ServerLog.self,
    where: #Predicate { $0.responseCode >= 500 }
)

4. FetchDescriptor 与 SortDescriptor

FetchDescriptor 是数据查询的描述符,相当于 SQL 的 WHERE + ORDER BY + LIMIT:

// 按时间倒序,最多取 10 条
let sort = SortDescriptor(\.timestamp, order: .reverse)
let descriptor = FetchDescriptor<ServerLog>(
    sortBy: [sort],
    fetchLimit: 10
)

// 过滤:只查状态码为 200 的请求
descriptor.predicate = #Predicate<ServerLog> { $0.responseCode == 200 }

let recentOK = try context.fetch(descriptor)

FetchDescriptor 完整参数:

参数作用说明
predicate过滤条件#Predicate 宏生成
sortBy排序规则SortDescriptor 数组,按顺序生效
fetchLimit最大返回数类似 SQL 的 LIMIT
fetchOffset偏移量分页使用,类似 SQL 的 OFFSET

5. #Predicate 宏过滤

#Predicate 是类型安全的过滤表达式,编译器会检查属性名和类型是否匹配。这是它相比传统 NSPredicate 的最大优势:写错了在编译期就会报错。

// 等值查询
let p1 = #Predicate<ServerLog> { $0.responseCode == 200 }

// 范围查询
let p2 = #Predicate<ServerLog> {
    $0.responseCode >= 400 && $0.responseCode < 500
}

// 字符串匹配
let p3 = #Predicate<ServerLog> {
    $0.endpoint.contains("api")
}

// 日期范围
let lastWeek = Date().addingTimeInterval(-7 * 24 * 3600)
let p4 = #Predicate<ServerLog> { $0.timestamp >= lastWeek }

// 组合条件
let p5 = #Predicate<ServerLog> {
    $0.responseCode == 200 && $0.timestamp >= lastWeek
}

6. @ModelActor 并发安全

SwiftData 的对象不是线程安全的。跨Actor传递 @Model 对象会触发 Sendable 检查失败。解决方式之一是使用 @ModelActor

@available(macOS 14, *)
@ModelActor
actor MetricsDataService {
    // 自动提供 modelContext 和 modelContainer

    func recordMetric(name: String, time: Double) throws {
        let metric = ServiceMetrics(serviceName: name, responseTime: time)
        modelContext.insert(metric)
        try modelContext.save()
    }

    func getAverageResponseTime(for name: String) throws -> Double {
        let predicate = #Predicate<ServiceMetrics> {
            $0.serviceName == name
        }
        let descriptor = FetchDescriptor<ServiceMetrics>(
            predicate: predicate
        )
        let results = try modelContext.fetch(descriptor)
        guard !results.isEmpty else { return 0.0 }
        return results.reduce(0.0) { $0 + $1.responseTime }
            / Double(results.count)
    }
}

@ModelActor 的作用类似于自动生成了一个隔离Actor,它保证:

  • 所有数据库操作都在同一个 serial queue 上执行
  • 不会发生并发写入冲突
  • 从外部通过 await 调用的方式访问,天然线程安全

7. @Relationship 级联删除

一对多关系使用 @Relationship 标记,支持删除策略配置:

@Model
final class Author {
    var name: String
    @Relationship(deleteRule: .cascade)
    var books: [Book]

    init(name: String) {
        self.name = name
        self.books = []
    }
}

@Model
final class Book {
    var title: String
    var author: Author?
    init(title: String) { self.title = title }
}
deleteRule效果
.nullify被关联对象的引用设为 nil
.cascade级联删除所有被关联对象
.noAction不采取任何操作(可能导致悬空引用)
.deny如果被关联对象存在,则阻止删除

8. Lightweight Migration (轻量迁移)

开发过程中你经常会给模型加字段。SwiftData 支持自动的轻量迁移,不需要手动写迁移逻辑:

场景: 给已有 ServerLog 加上 duration: TimeInterval 字段。

// 旧 schema
@Model final class ServerLog {
    var endpoint: String
    var responseCode: Int
}

// 新 schema:添加字段
@Model final class ServerLog {
    var endpoint: String
    var responseCode: Int
    var duration: Double = 0.0   // 新增字段带默认值
}

只需要:

  1. 新增属性提供默认值
  2. ModelConfiguration 设置 allowsSaveForward: true

SwiftData 会自动迁移已有数据。如果旧行缺少新列,会用默认值填充。

限制: 字段改名、字段类型变更、删除字段等复杂操作需要手动编写 MappingModel。轻量迁移只支持"加可选字段或带默认值字段"这类简单变更。


常见错误

以下错误来源于大量开发者在 SwiftData 实际项目踩坑后的总结,按出现频率排列。

错误 1: 忘记在 ModelContainer 中注册模型

症状: 编译通过,运行后 fetch 返回空数组,不报任何错误。

// ❌ 只注册了 ServerLog,但代码中查询了 ServiceMetrics
let container = try ModelContainer(for: ServerLog.self)
let descriptor = FetchDescriptor<ServiceMetrics>() // 编译不报错!
let results = try context.fetch(descriptor)        // 返回 []

修复:ModelContainerfor: 参数列出所有模型:

// ✅
let container = try ModelContainer(
    for: ServerLog.self, ServiceMetrics.self
)

错误 2: 在不同 Context 之间传递模型对象

症状: 运行崩溃 (crash),报错 Fault: Object belongs to a different context

// ❌ obj 属于 contextA,却在 contextB 里修改
let obj = try contextA.fetch(descriptor).first!
contextB.insert(obj)        // 直接崩溃

修复: 用唯一标识(如 UUID)重新获取对象:

// ✅ 传 ID,在目标 context 中重新查询
let objID = obj.id
let freshObj = try contextB.fetch(
    FetchDescriptor(predicate: #Predicate { $0.id == objID })
).first!

错误 3: #Predicate 中使用不支持的类型

症状: 运行时 EXC_BAD_ACCESS 崩溃,不报清晰的错误信息。

// ❌ 自定义 struct 不能直接在 #Predicate 中使用
struct Status { var code: Int }
let p = #Predicate<ServerLog> { $0.status == Status(code: 200) }

修复: 只用 SwiftData 原生支持的类型(Int, String, Date 等):

// ✅
let p = #Predicate<ServerLog> { $0.responseCode == 200 }

错误 4: 跨 Actor 传递 @Model 对象

症状: 编译报错 Instance of non-Sendable type 'ServerLog' in main-sendable closure

// ❌ @Model 对象不是 Sendable,不能跨 Actor 边界传递
func process(log: ServerLog) async {  // 编译错误
    actor.insert(log)
}

修复: 改用 @ModelActor 或在不同 Actor 间只传基本类型(ID):

// ✅ 用 @ModelActor 内部操作
await metricsService.recordMetric(name: "API", time: 100)

错误 5: @Model 类缺少显式初始化器

症状: 编译报错 'required' initializer in'@Model' class

// ❌ 没有写 init,编译器要求你提供
@Model
final class Config {
    var key: String
    var value: Int
    // 缺少 init!
}

修复: 提供至少一个 init 方法初始化所有非可选属性:

// ✅
@Model
final class Config {
    var key: String
    var value: Int

    init(key: String, value: Int) {
        self.key = key
        self.value = value
    }
}

Swift vs Rust/Python 对比

维度Swift (SwiftData)Rust (SQLx / Diesel)Python (SQLAlchemy)
声明方式@Model 宏 + final class结构体 + #[derive(Queryable)]sqlx::query!declarative_base() 子类
schema 管理自动建表,轻量迁移手动 migrations/ SQL 文件或 diesel migration generatealembic 迁移工具
查询方式FetchDescriptor + #Predicate类型安全 builder API 或原生 SQLQuery API / ORM 方法链
类型安全编译期强检查编译期强检查 (SQLx compile-time)运行期检查
并发安全@ModelActor 隔离Diesel pool / SQLx 连接池Session 线程本地
后端存储SQLite(默认)SQLite / PostgreSQL / MySQL几乎所有数据库
学习曲线低(纯 Swift 代码)高(需要理解连接池、生命周期)中(API 丰富但概念多)

核心差异: SwiftData 是 Apple 官方方案,专为 Apple 平台本地持久化设计,牺牲了跨库灵活性换取了极简 API。Rust 的 SQLx 在编译期就检查 SQL 语法正确性,适合后端服务。Python 的 SQLAlchemy 是目前最成熟的 ORM 之一,生态最广。

如果你开发的是 macOS/iOS 本地应用,SwiftData 是首选。如果你在写后端服务,Rust + SQLx 或 Python + SQLAlchemy 更合适。


动手练习 Level 1

目标: 定义一个 @Model 并插入一条数据。

定义一个 Student 模型,包含 name: Stringage: Intenrolled: Date。用 ModelContainerModelContext 将三个学生数据存入数据库。

// 在 AdvanceSample/Sources/AdvanceSample/SwiftDataSample.swift 中实现

@available(macOS 14, *)
@Model
final class Student {
    var name: String
    var age: Int
    var enrolled: Date

    init(name: String, age: Int) {
        self.name = name
        self.age = age
        self.enrolled = Date()
    }
}
查看参考答案
@available(macOS 14, *)
func studentSample() async {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try! ModelContainer(
        for: Student.self,
        configurations: config
    )
    let context = ModelContext(container)

    let s1 = Student(name: "Alice", age: 20)
    let s2 = Student(name: "Bob", age: 22)
    let s3 = Student(name: "Charlie", age: 21)

    context.insert(s1)
    context.insert(s2)
    context.insert(s3)
    try! context.save()

    let all = try! context.fetch(FetchDescriptor<Student>())
    print("Student count: \(all.count)") // 3
}

动手练习 Level 2

目标:FetchDescriptor + #Predicate 过滤数据。

在上一个练习的基础上,只查询年龄大于等于 21 岁的学生,按姓名排序输出。

查看参考答案
let predicate = #Predicate<Student> { $0.age >= 21 }
let sort = SortDescriptor(\.name, order: .forward)
let descriptor = FetchDescriptor<Student>(
    predicate: predicate,
    sortBy: [sort]
)
let filtered = try! context.fetch(descriptor)
for s in filtered {
    print("\(s.name), \(s.age)") // Alice, Bob, Charlie (sorted)
}

动手练习 Level 3

目标:@ModelActor 在后台线程安全地批量导入数据。

参考 SwiftDataSample.swiftMetricsDataService 的实现,创建一个 StudentImportService

  1. 定义 @ModelActor actor StudentImportService
  2. 提供 importStudents(_ names: [String]) async 方法
  3. 提供 getStudentCount() async -> Int 方法
  4. 在外部通过 Task 并发调用多次 importStudents,验证不会崩溃
查看参考答案
@available(macOS 14, *)
@ModelActor
actor StudentImportService {
    func importStudents(_ names: [String]) {
        for name in names {
            let student = Student(name: name, age: Int.random(in: 18...25))
            modelContext.insert(student)
        }
        try! modelContext.save()
    }

    func getStudentCount() -> Int {
        let result = try! modelContext.fetch(FetchDescriptor<Student>())
        return result.count
    }
}

// 使用
@available(macOS 14, *)
func runImportSample() async {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try! ModelContainer(
        for: Student.self,
        configurations: config
    )
    let service = StudentImportService(modelContainer: container)

    // 并发导入多批数据(@ModelActor 自动处理并发安全)
    await service.importStudents(["Alice", "Bob"])
    await service.importStudents(["Charlie", "Diana", "Eve"])

    let count = await service.getStudentCount()
    print("Total students: \(count)") // 5
}

故障排查 FAQ

Q1: fetch 返回空数组,但我明明插入了数据?

检查 ModelContainer 是否注册了你要查询的模型类型。ModelContainer(for: SomeModel.self) 中不声明的类型即使编译通过也无法查询。另外检查 isStoredInMemoryOnly: true 的容器在应用重启后数据会丢失。

Q2: 修改了模型属性后,应用启动崩溃 NSMigrationError

SwiftData 的 schema 发生变化时,如果旧数据的 SQLite 文件与新模型不匹配就会崩溃。临时开发时可以删除旧的 .sqlite 文件重建,或者配置 allowsSaveForward: true 启用自动迁移。

Q3: 在 #Predicate 中调用自定义方法报编译错误?

#Predicate 宏只支持有限的操作集合。不能使用自定义函数、闭包、或者非标准库类型。如果查询逻辑太复杂,可以先用 FetchDescriptor 拉回数据,再用 Swift 代码在内存中过滤。

Q4: 并发写入报 cannot be used in a Sendable context 错误?

@Model 对象不是 Sendable 的,不能跨 actor 传递。解决方案:在同一个 @ModelActor 内操作数据,或者只传递基本类型(如 UUID ID),在目标 actor 里重新 fetch 对象。

Q5: ModelContainer 初始化失败,报 SQLite error

检查数据库文件路径是否可写。如果用临时目录,确保路径已经创建。另外,如果用 Xcode 运行,检查沙盒权限。内存模式 (isStoredInMemoryOnly: true) 可以避免文件权限问题。


小结

  • @Model 宏将普通的 final class 转换为可持久化数据模型,编译器自动处理底层存储细节
  • ModelContainer 管理 SQLite 文件连接和模型 schema,可通过 ModelConfiguration 选择文件存储或内存存储
  • ModelContext 是 CRUD 操作的工作单元,直接修改对象属性后调用 save() 即可自动追踪变更
  • FetchDescriptor + #Predicate 提供类型安全的查询接口,支持过滤、排序、分页
  • @ModelActor 将数据库操作封装在 Actor 隔离域内,天然解决并发安全问题

术语表

术语英文说明
数据模型@Model用宏标记的类,声明哪些数据需要持久化
数据容器ModelContainer管理底层存储和模型 schema 的核心对象
数据上下文ModelContext执行插入、查询、修改、删除等操作的工作单元
查询描述符FetchDescriptor声明要查询什么数据,包括过滤和排序条件
谓词#Predicate类型安全的过滤表达式宏,用于筛选数据
模型Actor@ModelActor提供并发安全数据库操作的 Actor 抽象
关系@Relationship声明模型之间一对多或多对多关联的标记
轻量迁移Lightweight Migration自动处理简单 schema 变更(加字段)的机制
排序描述符SortDescriptor声明查询结果的排序方式

知识检查

题目 1: 下面哪个声明不会编译通过?为什么?

// A
@Model final class User {
    var name: String
    init(name: String) { self.name = name }
}

// B
@Model final class Item {
    var handler: () -> Void
}

// C
@Model final class Config {
    var key: String?
}
查看答案和解析

答案是 B。 闭包类型 () -> Void 不是 SwiftData 支持的可持久化类型。@Model 只支持基本类型(Int, String, Double, Bool, Date, Data, UUID 等)以及遵循 RawRepresentable 的枚举。A 和 C 都会正常编译,C 中可选类型是允许的。

题目 2: ModelActormodelContext 和手动创建的 ModelContext 有什么区别,为什么推荐在 Actor 中使用 @ModelActor

查看答案和解析

@ModelActor 自动生成的 modelContext 绑定在 Actor 的隔离域内,所有对这个 context 的操作都会经过 Actor 的 serial queue 调度,不会发生并发冲突。手动创建的 ModelContext 没有这种隔离保证,如果从多个线程同时操作同一个 context 会导致数据损坏。@ModelActor 相当于帮你自动处理了线程安全。

题目 3: 你给 ServerLog 模型增加了一个 duration: Double 字段后,已安装的应用启动时崩溃。列出至少两种修复办法。

查看答案和解析

两种修复办法:

  1. 开发阶段:删除旧的 SQLite 文件重建。用 FileManager.default.removeItem(at: oldDBURL) 或者让用户卸载重装。适用于还没有用户数据的开发/测试阶段。
  2. 发布阶段:启用轻量迁移。设置 ModelConfiguration(allowsSaveForward: true),并为新增的 duration 字段提供默认值 var duration: Double = 0.0。SwiftData 会自动为新列填充默认值,已有的行不会丢失。

继续学习

完成本章后,你已经掌握了用纯 Swift 代码管理本地持久化数据的能力。下一步继续阅读 环境配置 章节,学习如何管理 .env 环境变量和运行时配置。

扩展阅读: 想深入了解 SwiftData 和 SwiftUI 的结合?SwiftData 提供的 @Query 属性包装器可以在视图数据变化时自动刷新 UI,这是 SwiftUI + SwiftData 组合的核心特性。

环境配置

开篇故事

想象你进入一栋大楼,每一扇门背后有不同的区域。前台给你一叠门禁卡——每张卡上的编号决定了你能打开哪扇门。编号 "admin" 能打开所有门,编号 "guest" 只能进大厅。

环境变量 (environment variables) 就是这样的门禁卡。你的程序运行时,操作系统传递给它一组键值对,程序读到不同的值就能做出不同的行为。连接数据库的密码、调用第三方 API 的密钥、甚至程序应该用中文还是英文显示——全都可以通过环境变量控制。

在 Swift 中,最基础的读取方式是通过 ProcessInfo,但它的功能有限。如果要从 .env 文件批量加载配置,你需要 swift-dotenv 这样的第三方库。

AdvanceSample/Sources/AdvanceSample/DotenvySample.swift 中有完整的示例代码。让我们从最简单的开始。


本章适合谁

如果你写过以下任何一种代码,环境配置对你来说不是可选知识,而是必学技能:

  • 在代码里硬编码了 API 密钥或数据库密码
  • 开发时需要切换 "开发环境" 和 "生产环境"
  • 多人协作时因为每个人的本地配置不同导致运行结果不一致

本章适合有一定 Swift 基础的学习者。你需要理解可选类型 (optional) 和基本的文件读写概念。


你会学到什么

完成本章后,你可以:

  1. 使用 ProcessInfo.processInfo 读取操作系统级别的环境变量
  2. 使用 swift-dotenv 加载 .env 文件中的配置
  3. 理解动态成员查找 (dynamic member lookup) 的语法糖 @dynamicMemberLookup
  4. 明白为什么绝不应该把密钥写死在源代码里
  5. 排查常见的环境变量读取错误

前置要求

  • 掌握 Swift 基础语法,尤其是可选类型 (optional) 和 if let 安全解包
  • 理解 .env 文件的基本格式:每行一条 KEY=VALUE
  • 了解如何在命令行中设置环境变量(如 export MY_VAR=hello
  • 已阅读 JSON 处理文件操作 章节

第一个例子

打开 AdvanceSample/Sources/AdvanceSample/DotenvySample.swift,先看通过 ProcessInfo 读取环境变量的代码:

import Foundation

public func processInfoEnvSample() {
    startSample(functionName: "DotenvySample processInfoEnvSample")

    // 读取单个环境变量
    if let path = ProcessInfo.processInfo.environment["PATH"] {
        print("系统 PATH 变量: \(path)")
    } else {
        print("未找到该环境变量")
    }

    // 遍历所有环境变量
    let allEnv = ProcessInfo.processInfo.environment
    for (key, value) in allEnv {
        print("\(key): \(value)")
    }

    endSample(functionName: "DotenvySample processInfoEnvSample")
}

发生了什么?

  • ProcessInfo.processInfo 是 Swift 标准库提供的单例,封装了当前进程的信息
  • .environment 返回一个 [String: String] 字典,包含所有环境变量
  • 返回的是可选值,因为变量可能不存在

关键区别ProcessInfo 只能读取程序启动时已有的环境变量。它不会自动加载 .env 文件。要让 .env 生效,需要借助第三方库。


原理解析

1. ProcessInfo vs Dotenv.configure()

ProcessInfo.processInfo.environment 读取的是操作系统传入的环境变量。这些变量在进程启动时就已经确定,程序中无法修改它们。

Dotenv.configure() 则完全不同。它会:

  1. 在当前工作目录查找 .env 文件
  2. 解析文件中的每一行(忽略注释和空行)
  3. 将解析出的键值对写入进程内存(通过 setenv 底层调用)
import SwiftDotenv

try Dotenv.configure()  // 加载 .env 文件
print(Dotenv.apiKey)    // 使用动态成员查找读取 API_KEY

2. 动态成员查找 (Dynamic Member Lookup)

Dotenv.apiKey 看起来像是在访问一个普通属性,实际上并不是。这是 @dynamicMemberLookup 特性带来的语法糖。

在没有语法糖的情况下,等价的写法是:

let key = Dotenv["API_KEY"]  // 下标访问
print(key?.stringValue)

加上 @dynamicMemberLookup 后,编译器自动把 .apiKey 转换为 ["API_KEY"](自动转大写加下划线),让代码更简洁。

3. 安全考量

永远不要 把 API 密钥、数据库密码等敏感信息写死在源代码里。原因很简单:

  • 源代码会被提交到版本管理系统(如 Git),一旦推送就可能泄露
  • 多个开发者共用同一份代码,但各自的本地配置应该不同

正确的做法:把配置写在 .env 文件中,然后把这个文件加入 .gitignore。每个人维护自己本地的 .env,不会冲突也不会泄露。


常见错误

错误一:.env 文件不存在

can't load .env config

这是最常见的报错。Dotenv.configure() 找不到 .env 文件时会抛出错误。

原因.env 文件不在当前工作目录下。Dotenv.configure() 从进程的工作目录开始搜索,而不是从你的源代码目录。

解决方法:在命令行运行 swift run 时确认 .env 在项目根目录。如果是 Xcode 运行,检查 Scheme 设置中的 "Working Directory"。

错误二:格式错误,值用了引号

# 错误写法
API_KEY="my-secret-key"

# 正确写法
API_KEY=my-secret-key

swift-dotenv 默认不会去掉引号。如果你写了 API_KEY="hello",读出来的值是 "hello"(带引号),而不是 hello

错误三:动态成员查找返回 nil

print(Dotenv.apiKey)  // nil

Dotenv.apiKey 返回 nil 的可能原因:

  1. .env 文件没有加载成功(检查 Dotenv.configure() 是否抛出错误)
  2. .env 中的键名不是 API_KEY.apiKey 会自动转为 API_KEY,如果实际键名不同就找不到)
  3. 值类型为 .null 而非字符串

错误四:环境变量冲突

如果系统环境变量和 .env 文件中有同名键,系统环境变量的优先级更高。Dotenv.configure() 不会覆盖已有的值。


Swift vs Rust/Python 对比

特性Swift (swift-dotenv)Rust (dotenvy)Python (python-dotenv)
安装方式SPM 依赖Cargo.toml 依赖pip install python-dotenv
加载函数Dotenv.configure()dotenvy::dotenv()load_dotenv()
读取方式Dotenv.apiKey / Dotenv["API_KEY"]dotenvy::get("API_KEY")os.environ.get("API_KEY")
类型系统强类型,需要 .stringValue 转换强类型,返回 Result弱类型,返回字符串或 None
动态成员查找支持(@dynamicMemberLookup不支持不支持
是否覆盖已有变量可选(override=True

Swift 的独特优势@dynamicMemberLookup 让你用属性访问语法读取环境变量,代码更优雅。这是 Swift 语言特性带来的便利,Rust 和 Python 都没有。


动手练习 Level 1

使用 ProcessInfo 读取系统环境变量 HOME(你的主目录路径),打印出来。

点击查看答案
import Foundation

if let home = ProcessInfo.processInfo.environment["HOME"] {
    print("主目录: \(home)")
} else {
    print("未找到 HOME 环境变量")
}

动手练习 Level 2

在项目根目录创建 .env 文件,写入以下内容:

DATABASE_URL=postgres://localhost:5432/mydb
DEBUG_MODE=true

使用 Dotenv.configure() 加载后读取 DATABASE_URL

点击查看答案
import SwiftDotenv

do {
    try Dotenv.configure()
    if let dbUrl = Dotenv["DATABASE_URL"]?.stringValue {
        print("数据库连接: \(dbUrl)")
    }
} catch {
    print("加载 .env 失败: \(error)")
}

动手练习 Level 3

使用动态成员查找语法读取 DEBUG_MODE 环境变量。提示:.debugMode 会自动映射为 DEBUG_MODE

点击查看答案
import SwiftDotenv

try Dotenv.configure()

// 使用动态成员查找(驼峰命名会自动转为大写加下划线)
let debugMode = Dotenv.debugMode?.stringValue
print("调试模式: \(debugMode ?? "未设置")")

// 等价于
// let debugMode = Dotenv["DEBUG_MODE"]?.stringValue

故障排查 FAQ

Q1. 为什么 Dotenv.configure() 报错了?

检查三点:.env 文件是否存在、文件路径是否正确、文件内容格式是否正确(每行 KEY=VALUE,不要用引号包裹值)。

Q2. ProcessInfo.processInfo.environment["MY_VAR"] 返回 nil,但我明明设过了。

确认你在哪个终端设置了变量。export MY_VAR=hello 只对当前终端会话有效。如果你另开了一个终端窗口或者直接用 Xcode 运行,Xcode 看不到那个变量。

Q3. .env 文件应该加入版本控制吗?

不应该。在 .gitignore 中添加 .env。如果需要团队协作,可以提交一个 .env.example 作为模板,里面只写变量名不写真实值。

Q4. Swift 程序运行时可以修改环境变量吗?

可以改,用 setenv(),但 不推荐。环境变量应该在整个程序生命周期内保持不变。如果需要在运行时切换配置,考虑使用一个自定义的配置类,而不是修改环境变量。

Q5. Dotenv.apiKeyDotenv["API_KEY"] 有什么区别?

功能上没有任何区别。.apiKey 是语法糖,编译器内部会自动转为 ["API_KEY"] 下标访问。命名规则是:驼峰转大写+下划线,mySecretValueMY_SECRET_VALUE

Q6. 如何在 Xcode 中设置环境变量?

编辑 Scheme → Run → Arguments → Environment Variables,在那里添加键值对。这种方式设置的环境变量只在通过 Xcode 运行时生效。


小结

  • ProcessInfo.processInfo.environment 是标准库提供的环境变量读取方式,返回 [String: String] 字典
  • swift-dotenv 可以从 .env 文件加载配置到进程内存,适合管理多组环境变量
  • @dynamicMemberLookup 允许用属性访问语法读取环境变量,让代码更简洁
  • 敏感信息(API 密钥、数据库密码)绝不应该写死在源代码中
  • .env 文件应加入 .gitignore,避免泄露到版本控制系统

术语表

术语说明
ProcessInfoSwift 标准库类,封装当前进程信息,包括环境变量、参数、主机名等
Dotenv第三方库 swift-dotenv 提供的核心类型,用于加载和访问 .env 文件中的配置
动态成员查找 (Dynamic Member Lookup)Swift 语言特性 (@dynamicMemberLookup),允许在运行时动态解析属性名
@dynamicMemberLookup属性标记,告诉编译器把属性访问转换为下标调用,如 .apiKey["API_KEY"]
.env 文件纯文本配置文件,每行一条键值对,格式为 KEY=VALUE,广泛用于环境变量管理

知识检查

问题 1: ProcessInfo.processInfo.environment 返回什么类型?

查看答案

[String: String] 字典(即 ProcessInfo.Environment,底层等价于 [String: String])。

问题 2: Dotenv.debugMode 实际对应 .env 文件中的哪个键名?

查看答案

DEBUG_MODE@dynamicMemberLookup 自动将驼峰命名转为大写加下划线格式。

问题 3: 为什么说把 API 密钥写死在代码里是危险的做法?

查看答案

因为源代码会被提交到版本管理系统(如 Git)。一旦推送到远程仓库(尤其是公开的 GitHub),任何人都能看到这些密钥,可能导致未授权访问和数据泄露。正确做法是放入 .env 文件并加入 .gitignore


继续学习

完成了环境配置的学习,你已经掌握了 Swift 项目中管理敏感信息的正确方式。接下来可以:

SwiftNIO 异步网络基础

开篇故事

想象你在经营一家快递公司。传统的做法是:每个快递员负责一个客户,从取件到送达全程跟踪,客户越多,快递员越多。这种方式效率低下——快递员大部分时间在等客户签字、等交通灯、等收件人出现。

现代快递公司改用"分拣中心"模式:快递员只负责取件和送达两个动作,中间的运输、分拣由专职团队处理。一个快递员可以同时服务多个客户,效率翻倍。

SwiftNIO 就是 Swift 世界里的"分拣中心"。它用 EventLoop(事件循环)管理所有网络连接,一个线程可以处理成千上万的并发请求。你不需要为每个连接创建一个线程,SwiftNIO 会自动帮你调度。

本章要教你的,就是如何用 SwiftNIO 构建这种高效率的网络应用。

本章适合谁

如果你满足以下任一情况,这一章就是为你准备的:

  • 你需要构建 TCP 或 HTTP 服务器(如聊天服务器、API 服务器)
  • 你想理解 Vapor、Hummingbird 等 Web 框架底层原理
  • 你对异步网络编程感兴趣,想知道 EventLoop、Channel 怎么工作
  • 你想让代码同时跑在 macOS 和 Linux 上,跨平台部署

本章面向已经掌握 Swift async/await 基础的开发者。你需要知道 Task、async 函数的基本用法。

你会学到什么

完成本章后,你将掌握以下内容:

  • EventLoop(事件循环):SwiftNIO 如何在单线程上处理多个连接
  • Channel(通道):网络连接的生命周期和管道模型
  • ChannelHandler(处理器):如何编写入站/出站数据处理逻辑
  • ByteBuffer(字节缓冲区):零拷贝读写、切片操作、高效内存管理
  • ServerBootstrap(服务器启动器):创建 TCP 服务器的完整流程
  • 跨平台部署:swift-nio 在 macOS 和 Linux 上的行为差异

前置要求

在开始之前,请确保你已掌握以下内容:

  • Swift async/await:Task、async 函数、await 关键字
  • Swift 并发基础:理解 Task.sleep 与 Thread.sleep 的区别
  • 网络基础:知道 TCP、端口的概念
  • 错误处理:do-catch-try 模式

运行环境要求:

  • macOS 12.0+ 或 Linux(Ubuntu 22.04+)
  • Swift 6.0+
  • swift-nio 已在 Package.swift 中声明依赖

第一个例子

我们先来看一个最基础的例子:创建一个 Echo Server。它的功能很简单——收到什么消息,就原样发回去。就像你对着山谷喊话,山谷回声一样。

这段代码来自 AdvanceSample/Sources/AdvanceSample/SwiftNIOSample.swift

import NIOCore
import NIOPosix

// 创建 Echo Server 的核心代码
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)

let bootstrap = ServerBootstrap(group: group)
    .childChannelInitializer { channel in
        // 给每个连接添加 EchoHandler
        channel.pipeline.addHandler(EchoHandler())
    }

// 绑定端口并启动
let channel = try bootstrap.bind(host: "127.0.0.1", port: 8080).wait()
print("Server started on port \(channel.localAddress?.port ?? 0)")

运行这段代码后,你可以用 telnet 连接测试:

telnet 127.0.0.1 8080
# 输入任意文字,服务器会原样返回

原理解析

EventLoop:SwiftNIO 的心脏

EventLoop 是 SwiftNIO 的核心概念。它就像一个永不停止的循环,不断检查"有没有新事件发生"。

while true {
    for connection in activeConnections {
        if connection.hasData {
            handleData(connection)
        }
        if connection.hasError {
            handleError(connection)
        }
    }
}

关键特性

  • 单线程处理多连接:一个 EventLoop 可以管理数千个连接,避免线程爆炸
  • 非阻塞 I/O:没有数据时不等待,立即处理其他连接
  • 任务提交:可以用 submit {} 向 EventLoop 提交任务
let eventLoop = group.next()

// 向 EventLoop 提交任务
let future = eventLoop.submit {
    return "Task result"
}

// 获取结果(阻塞等待)
let result = try future.wait()

Channel:网络连接的管道

Channel 代表一个网络连接(TCP 连接)。它不是简单的 socket,而是由多个 Handler 组成的管道(Pipeline)。

入站数据流向:Socket → ByteHandler → DecodeHandler → BusinessHandler
出站数据流向:BusinessHandler → EncodeHandler → ByteHandler → Socket

Channel 生命周期

  1. channelRegistered:Channel 注册到 EventLoop
  2. channelActive:连接建立成功
  3. channelRead:收到数据
  4. channelReadComplete:一批数据读完
  5. channelInactive:连接关闭
  6. channelUnregistered:Channel 从 EventLoop 移除

ChannelHandler:数据处理单元

ChannelHandler 是你编写业务逻辑的地方。分为两类:

  • InboundHandler:处理入站数据(如解码、业务逻辑)
  • OutboundHandler:处理出站数据(如编码、发送)
final class EchoHandler: ChannelInboundHandler {
    typealias InboundIn = ByteBuffer  // 输入类型
    typealias InboundOut = ByteBuffer // 输出类型
    
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        // 把收到的数据原样写回
        context.write(data, promise: nil)
    }
    
    func channelReadComplete(context: ChannelHandlerContext) {
        // 刷新缓冲区,实际发送数据
        context.flush()
    }
    
    func errorCaught(context: ChannelHandlerContext, error: Error) {
        print("Error: \(error)")
        context.close(promise: nil)
    }
}

ByteBuffer:高效的字节容器

ByteBuffer 是 SwiftNIO 的核心数据结构,用于存储网络数据。它比 Data 或 [UInt8] 更高效。

关键特性

  • 零拷贝切片:slice 操作不复制数据,只移动指针
  • 自动扩容:写入超过容量时自动扩展
  • 读写指针分离:readerIndex 和 writerIndex 独立管理
let allocator = ByteBufferAllocator()
var buffer = allocator.buffer(capacity: 256)

// 写入数据
buffer.writeString("Hello")
buffer.writeInteger(42 as Int32)

// 读取数据
let text = buffer.readString(length: 5)  // "Hello"
let number = buffer.readInteger(as: Int32.self)  // 42

// 切片(零拷贝)
buffer.writeString("SliceDemo")
let slice = buffer.getSlice(at: 0, length: 9)
// slice 和 buffer 共享底层内存

常见错误

错误原因解决方案
Blocking operation on EventLoop在 EventLoop 线程执行 Thread.sleep 或同步 I/O使用 Task.sleep 或 eventLoop.scheduleTask
Channel closed before write completed写入数据后立即关闭连接使用 writeAndFlush 的 promise 等待完成后再 close
ByteBuffer readIndex out of bounds读取超过可读范围检查 buffer.readableBytes 后再读取
EventLoopGroup shutdown leak未调用 shutdownGracefully在应用退出前调用 group.shutdownGracefully
ChannelHandler type mismatchInboundIn 类型与 Pipeline 不匹配确保 Handler 的 typealias 与上游输出类型一致

错误示例 1:阻塞 EventLoop

// ❌ 错误写法
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
    Thread.sleep(forTimeInterval: 1.0)  // 阻塞 EventLoop!
    context.write(data, promise: nil)
}

// ✅ 正确写法
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
    context.eventLoop.scheduleTask(in: .seconds(1)) {
        context.write(data, promise: nil)
    }
}

错误示例 2:过早关闭连接

// ❌ 错误写法
context.write(data, promise: nil)
context.close(promise: nil)  // 数据可能还没发送完

// ✅ 正确写法
context.writeAndFlush(data).whenComplete { _ in
    context.close(promise: nil)
}

Swift vs Rust/Python 对比

概念Swift (SwiftNIO)Rust (tokio)Python (asyncio)
事件循环EventLoopRuntimeEvent Loop
连接抽象ChannelTcpStreamStreamReader
数据缓冲ByteBufferBytesMutbytes
处理器ChannelHandlercodec::FramedProtocol
任务提交eventLoop.submit {}tokio::spawnasyncio.create_task
Future/PromiseEventLoopFutureFutureasyncio.Future
异步等待future.wait().awaitawait

关键差异

  • SwiftNIO 的 ByteBuffer 提供零拷贝切片,性能接近 Rust
  • Swift 的 async/await 与 SwiftNIO 需要通过 NIOLoopBoundBox 桥接
  • Python asyncio 是单线程,SwiftNIO 支持 MultiThreadedEventLoopGroup

动手练习 Level 1

任务:修改 EchoHandler,让它在返回数据前加上 "Echo: " 前缀。

例如:客户端发送 "Hello",服务器返回 "Echo: Hello"。

// 提示:你需要读取 ByteBuffer 内容,修改后写回
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
    var buffer = unwrapInboundIn(data)
    if let text = buffer.readString(length: buffer.readableBytes) {
        var newBuffer = context.channel.allocator.buffer(capacity: 256)
        newBuffer.writeString("Echo: \(text)")
        context.write(wrapInboundOut(newBuffer), promise: nil)
    }
}

动手练习 Level 2

任务:创建一个简单的聊天服务器,支持以下功能:

  1. 多个客户端连接
  2. 一个客户端发送的消息,所有客户端都能收到
  3. 客户端断开时通知其他客户端

提示

  • 需要一个共享的 activeChannels: [Channel] 数组
  • 使用 ChannelHandlerContext.channel 记录连接
  • channelActive 时添加,channelInactive 时移除

动手练习 Level 3

任务:实现一个简单的 HTTP 服务器,返回固定响应:

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 13

Hello, World!

提示

  • HTTP 是基于 TCP 的文本协议
  • 需要解析请求行(GET / HTTP/1.1)
  • 响应必须包含正确的 Content-Length
点击查看 Level 3 参考代码
final class HTTPHandler: ChannelInboundHandler {
    typealias InboundIn = ByteBuffer
    
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        var request = unwrapInboundIn(data)
        if let requestText = request.readString(length: request.readableBytes) {
            // 简单检查是否是 HTTP GET 请求
            if requestText.hasPrefix("GET") {
                let response = "HTTP/1.1 200 OK\r\n" +
                    "Content-Type: text/plain\r\n" +
                    "Content-Length: 13\r\n\r\n" +
                    "Hello, World!"
                
                var buffer = context.channel.allocator.buffer(capacity: 256)
                buffer.writeString(response)
                context.writeAndFlush(wrapInboundOut(buffer), promise: nil)
            }
        }
    }
}

故障排查 FAQ

Q: SwiftNIO 编译失败,提示找不到 NIOCore 模块

A: 检查 Package.swift 是否正确声明 swift-nio 依赖:

.package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.92.0"))

并在 target dependencies 中添加:

.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio")

Q: 服务器启动后立即退出,没有等待连接

A: SwiftNIO 的 bind 返回 Future,需要等待或保持 EventLoopGroup 运行:

let channel = try bootstrap.bind(host: "127.0.0.1", port: 8080).wait()
// 不要立即调用 group.shutdownGracefully
// 保持 channel 运行直到需要退出

Q: ByteBuffer.readString 返回 nil

A: 检查 readableBytes 是否足够,readString 会移动 readIndex:

if buffer.readableBytes >= expectedLength {
    let text = buffer.readString(length: expectedLength)
}

Q: 如何在 Linux 上测试 SwiftNIO 服务器?

A: SwiftNIO 完全支持 Linux。使用相同代码,编译后用 telnet 或 nc 测试:

swift build
.build/debug/your-server &
telnet 127.0.0.1 8080

Q: ChannelPipeline.addHandler 报错类型不匹配

A: 确保 Handler 的 InboundIn 类型与 Pipeline 中前一个 Handler 的输出类型一致:

// 如果上一个 Handler 输出 ByteBuffer
typealias InboundIn = ByteBuffer

小结

本章你学会了 SwiftNIO 的核心概念:

  • EventLoop:单线程管理多连接,非阻塞事件循环
  • Channel:网络连接抽象,由 Pipeline 和 Handler 组成
  • ChannelHandler:入站/出站数据处理单元,编写业务逻辑的地方
  • ByteBuffer:高效字节容器,零拷贝切片,读写指针分离
  • ServerBootstrap:创建 TCP 服务器的启动器模式

SwiftNIO 是 Vapor、Hummingbird 等 Web 框架的基础。掌握它,你就能理解这些框架的底层原理,也能自己构建高性能网络服务。

术语表

中文英文说明
事件循环EventLoop单线程事件分发器,管理多个连接
通道Channel网络连接抽象,包含 Pipeline
管道PipelineHandler 组成的处理链
处理器ChannelHandler数据处理单元,分入站/出站
字节缓冲区ByteBuffer高效内存容器,零拷贝设计
启动器Bootstrap服务器或客户端创建工具
FutureEventLoopFuture异步结果容器
PromiseEventLoopPromiseFuture 的写入端
非阻塞Non-blocking不等待 I/O 完成,立即返回

知识检查

  1. EventLoop 如何在单线程上处理多个网络连接?

  2. ChannelPipeline 中 Handler 的排列顺序对数据处理有什么影响?

  3. 为什么不应该在 ChannelHandler 中使用 Thread.sleep?

点击查看答案与解析
  1. EventLoop 使用非阻塞 I/O 和事件驱动模式:它不断轮询所有活跃连接,检查是否有数据到达、连接关闭等事件。当某个连接有数据时,立即处理,不等待;没有数据时,跳过该连接,处理其他连接。这样单线程就能管理数千连接,避免了传统"一连接一线程"的资源浪费。

  2. 顺序决定数据流向:入站数据按 Pipeline 从头到尾经过每个 InboundHandler;出站数据从尾到头经过每个 OutboundHandler。例如:ByteHandler → DecodeHandler → BusinessHandler,入站数据先被 ByteHandler 处理(原始字节),再被 DecodeHandler 解码(结构化数据),最后到 BusinessHandler(业务逻辑)。顺序错误会导致类型不匹配。

  3. Thread.sleep 会阻塞整个 EventLoop:一个 EventLoop 管理数千连接,如果某个 Handler 阻塞,所有连接都会被卡住。正确做法是使用 eventLoop.scheduleTask(in: .seconds(1)) 或 Swift Concurrency 的 Task.sleep,让 EventLoop 继续处理其他连接,定时任务完成后才执行后续逻辑。

继续学习

下一章: SwiftNIO async/await 集成 - 学习如何将 SwiftNIO 与现代 Swift 并发模型结合

返回: 高级进阶概览

SwiftNIO async/await 集成

开篇故事

你刚学会骑自行车,现在教练让你骑摩托车。自行车靠体力蹬踏,摩托车靠油门控制。虽然都是"两轮交通工具",但操作方式完全不同。

SwiftNIO 就像自行车——它有自己的 EventLoopFuture/Promise 体系。Swift async/await 就像摩托车——它用 Task、await 关键字管理异步。两者都是异步编程,但语法不同。

本章教你的,就是如何把这两套"交通工具"结合起来,让 SwiftNIO 跑在 async/await 的"摩托车"上。你不必扔掉 SwiftNIO 的知识,而是学会用更现代的方式驾驭它。

本章适合谁

如果你满足以下任一情况,这一章就是为你准备的:

  • 你已经掌握上一章的 SwiftNIO 基础(EventLoop、Channel、ByteBuffer)
  • 你习惯了 Swift async/await 语法,觉得 Future/Promise 很繁琐
  • 你想在新项目中用 async/await,但又需要 SwiftNIO 的网络能力
  • 你遇到了 "Blocking operation on EventLoop" 错误,想知道正确做法

你会学到什么

完成本章后,你将掌握以下内容:

  • Future → async 转换:如何把 EventLoopFuture 变成 awaitable
  • Task 与 EventLoop 桥接:在 async 函数中安全使用 SwiftNIO
  • NIOLoopBoundBox:跨 Actor 安全访问 EventLoop-bound 值
  • NIOAsyncChannel:SwiftNIO 2.0+ 的现代 async/await API
  • 避免阻塞 EventLoop:正确的异步等待方式

前置要求

在开始之前,请确保你已掌握以下内容:

  • Swift async/await:Task、async 函数、await 关键字、TaskGroup
  • SwiftNIO 基础:EventLoop、EventLoopFuture、Channel(上一章内容)
  • Swift 并发安全:Sendable 协议、Actor 隔离概念

运行环境要求:

  • macOS 12.0+ 或 Linux(Ubuntu 22.04+)
  • Swift 6.0+(Strict Concurrency 模式)
  • swift-nio 2.92.0+(支持 NIOAsyncChannel)

第一个例子

先看一个经典问题:你有一个 SwiftNIO 的 Future,但你想在 async 函数里 await 它。

这段代码来自 AdvanceSample/Sources/AdvanceSample/SwiftNIOSample.swift

import NIOCore
import NIOPosix

// SwiftNIO 的 Future 方式
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let eventLoop = group.next()

let future = eventLoop.submit {
    return "Result from EventLoop"
}

// 传统方式:阻塞等待
let result = try future.wait()  // ⚠️ 会阻塞当前线程

// 现代方式:async/await 桥接
func getResult() async throws -> String {
    // 需要特殊桥接方式...
}

原理解析

EventLoopFuture vs async/await

SwiftNIO 的 EventLoopFuture<T> 是传统的异步容器:

  • 创建时不立即完成,等待 EventLoop 执行
  • 通过 .whenSuccess {}.whenFailure {} 回调处理结果
  • .wait() 会阻塞当前线程,不能在 EventLoop 线程调用

Swift async/await 是现代异步模型:

  • await 暂停当前函数,不阻塞线程
  • Task { } 创建异步任务
  • 编译器自动管理挂起和恢复

核心矛盾

  • SwiftNIO 的很多 API 返回 EventLoopFuture
  • async 函数需要 await,而不是 .wait()
  • 直接 .wait() 在 async 函数里会阻塞底层线程,违反 async 设计

桥接策略 1:withCheckedContinuation

Foundation 提供了 withCheckedContinuation,可以把任何回调式 API 转成 async:

import NIOCore

// 把 Future 转成 async
extension EventLoopFuture {
    func asyncValue() async throws -> Value {
        try await withCheckedThrowingContinuation { continuation in
            self.whenComplete { result in
                switch result {
                case .success(let value):
                    continuation.resume(returning: value)
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        }
    }
}

// 使用示例
func connectAsync() async throws -> Channel {
    let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
    let bootstrap = ServerBootstrap(group: group)
    
    let channelFuture = bootstrap.bind(host: "127.0.0.1", port: 8080)
    
    // 使用桥接,不阻塞线程
    return try await channelFuture.asyncValue()
}

桥接策略 2:NIOLoopBoundBox(推荐)

SwiftNIO 2.0+ 提供了 NIOLoopBoundBox,专门解决跨 Actor/Task 访问问题:

import NIOCore

// NIOLoopBoundBox 保证跨 Actor 安全
final class ConnectionManager: Actor {
    private let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
    private var channels: [NIOLoopBoundBox<Channel>] = []
    
    func createConnection() async throws -> Channel {
        let eventLoop = eventLoopGroup.next()
        let bootstrap = ClientBootstrap(group: eventLoopGroup)
            .channelInitializer { channel in
                channel.pipeline.addHandler(MyHandler())
            }
        
        let channel = try await bootstrap.connect(
            host: "127.0.0.1",
            port: 8080
        ).get()  // NIO 2.0+ 支持 .get() 桥接
        
        // 用 NIOLoopBoundBox 包装,保证 Sendable
        let boxedChannel = NIOLoopBoundBox(channel, eventLoop: eventLoop)
        channels.append(boxedChannel)
        
        return channel
    }
}

桥接策略 3:NIOAsyncChannel(最新)

SwiftNIO 2.40+ 提供了全新的 NIOAsyncChannel,完全基于 async/await 设计:

import NIOCore
import NIOPosix

// 使用 NIOAsyncChannel 创建 async 服务器
func startAsyncServer() async throws {
    let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
    
    let serverChannel = try await ServerBootstrap(group: group)
        .bind(host: "127.0.0.1", port: 8080)
        .map { channel in
            // 创建 NIOAsyncChannel
            NIOAsyncChannel(
                wrappingChannelSynchronously: channel,
                configuration: .init()
            )
        }.get()
    
    // async 方式处理连接
    try await withThrowingDiscardingTaskGroup { group in
        for try await connection in serverChannel.inboundStream {
            group.addTask {
                try await handleConnection(connection)
            }
        }
    }
}

func handleConnection(_ connection: NIOAsyncChannel) async throws {
    for try await data in connection.inboundStream {
        // async 方式处理数据
        try await connection.outboundStream.write(data)
    }
}

错误陷阱:阻塞 EventLoop

这是最常见的错误,也是最危险的:

// ❌ 绝对错误!
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
    Task {
        // Task 默认不在 EventLoop 上
        // await 会暂停 Task,但不影响 EventLoop
        
        // 但如果在 Task 里调用 wait()...
        let result = try someFuture.wait()  // 💥 阻塞 EventLoop!
    }
}

// ✅ 正确做法
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
    // 把工作提交到 EventLoop,让它调度
    context.eventLoop.execute {
        // 这里在 EventLoop 上,可以安全操作 Channel
    }
    
    // 或者用 async 桥接
    let future = someOperation(context)
    Task {
        try await future.asyncValue()  // 不阻塞,正确等待
    }
}

常见错误

错误原因解决方案
Blocking operation on EventLoop在 EventLoop 线程调用 wait() 或 Thread.sleep使用 Task.sleep 或 continuation 桥接
Actor isolation crossingChannel 不符合 Sendable,跨 Actor 访问用 NIOLoopBoundBox 包装
Future.wait() in async functionwait() 阻塞底层线程,违反 async 设计用 continuation 或 NIOAsyncChannel
NIOAsyncChannel not foundswift-nio 版本过低升级到 2.40.0+
Task detached from EventLoopTask.detached 不继承 EventLoop context用 Task { } 继承 context

错误示例 1:wait() 在 EventLoop

// ❌ 错误
final class MyHandler: ChannelInboundHandler {
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        // channelRead 在 EventLoop 上执行
        let future = context.channel.write(data)
        try! future.wait()  // 💥 阻塞整个 EventLoop!
    }
}

// ✅ 正确
final class MyHandler: ChannelInboundHandler {
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        // 用 promise 或回调
        context.writeAndFlush(data).whenComplete { result in
            switch result {
            case .success:
                print("Written")
            case .failure(let error):
                print("Error: \(error)")
            }
        }
    }
}

错误示例 2:Channel 跨 Actor

// ❌ 错误 - Channel 不符合 Sendable
actor ConnectionPool {
    var activeChannels: [Channel] = []  // 💥 Channel 不是 Sendable
    
    func add(channel: Channel) {
        activeChannels.append(channel)  // 跨 Actor 传递非 Sendable
    }
}

// ✅ 正确 - 用 NIOLoopBoundBox 包装
actor ConnectionPool {
    var activeChannels: [NIOLoopBoundBox<Channel>] = []
    
    func add(channel: Channel, eventLoop: EventLoop) {
        let boxed = NIOLoopBoundBox(channel, eventLoop: eventLoop)
        activeChannels.append(boxed)  // NIOLoopBoundBox 是 Sendable
    }
    
    func writeToAll(data: ByteBuffer) async throws {
        for box in activeChannels {
            try await box.withValue { channel in
                // 在正确的 EventLoop 上操作
                channel.writeAndFlush(data, promise: nil)
            }
        }
    }
}

Swift vs Rust/Python 对比

概念Swift (SwiftNIO + async)Rust (tokio + async)Python (asyncio)
Future 桥接continuationasync fn 自动兼容await 自动兼容
跨 Actor 安全NIOLoopBoundBoxArc无 Actor 模型
线程安全容器@unchecked SendableSend trait无类型约束
async 服务器NIOAsyncChanneltokio::net::TcpListenerasyncio.start_server
阻塞检测编译警告blocking!()无自动检测
任务继承 contextTask 默认继承tokio::spawnasyncio.create_task

关键差异

  • Swift 的 Actor 模型比 Rust 的 Arc 更严格,需要显式 Sendable
  • Python 的 asyncio 没有 Actor,跨线程访问靠人工约定
  • SwiftNIO 的 NIOLoopBoundBox 是独有设计,解决 EventLoop + Actor 冲突

动手练习 Level 1

任务:为 EventLoopFuture 写一个 async 扩展方法。

要求:

  1. 命名为 asyncResult()
  2. 正确处理 success 和 failure
  3. withCheckedThrowingContinuation
extension EventLoopFuture {
    func asyncResult() async throws -> Value {
        // 你的实现...
    }
}
点击查看参考答案
extension EventLoopFuture {
    /// 将 EventLoopFuture 转换为 async/await 兼容
    func asyncResult() async throws -> Value {
        try await withCheckedThrowingContinuation { continuation in
            self.whenComplete { result in
                switch result {
                case .success(let value):
                    continuation.resume(returning: value)
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        }
    }
}

动手练习 Level 2

任务:写一个 async Echo Server,使用 NIOAsyncChannel。

要求:

  1. 监听端口 9000
  2. 每个连接用独立 Task 处理
  3. 使用 for try await 读取入站数据
点击查看参考答案
import NIOCore
import NIOPosix

func startAsyncEchoServer() async throws {
    let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
    
    let server = try await ServerBootstrap(group: group)
        .childChannelInitializer { channel in
            channel.pipeline.addHandler(EchoHandler())
        }
        .bind(host: "127.0.0.1", port: 9000)
        .asyncResult()
    
    print("Async Echo Server started on port 9000")
    
    // 保持服务器运行
    try await server.closeFuture.asyncResult()
}

动手练习 Level 3

任务:实现一个 Actor 管理的连接池,支持:

  1. addConnection(channel: Channel)
  2. broadcast(message: String) - 向所有连接发送消息
  3. removeConnection(channel: Channel)
  4. 正确使用 NIOLoopBoundBox 保证 Sendable

提示:Actor 需要跨 Actor 访问 EventLoop-bound 值,NIOLoopBoundBox.withValue 是关键。

故障排查 FAQ

Q: Task.sleep 和 Thread.sleep 有什么区别?

A: Task.sleep 暂停当前 Task,不阻塞底层线程;Thread.sleep 阻塞整个线程。在 EventLoop 上用 Thread.sleep 会卡住所有连接。用 Task.sleep 或 eventLoop.scheduleTask。


Q: NIOLoopBoundBox.withValue 是什么?

A: 它保证操作在正确的 EventLoop 上执行。Channel 只能在创建它的 EventLoop 上修改,withValue 自动切换到正确的 EventLoop。

try await box.withValue { channel in
    // 这里在 channel 的 EventLoop 上执行
    channel.writeAndFlush(data, promise: nil)
}

Q: 为什么 Channel 不是 Sendable?

A: Channel 绑定到特定 EventLoop,跨线程/Actor 访问会破坏 EventLoop 的单线程假设。NIOLoopBoundBox 包装后变成 Sendable,通过 withValue 保证安全访问。


Q: NIOAsyncChannel 和普通 Channel 有什么区别?

A: NIOAsyncChannel 提供 async/await 接口:

  • inboundStream 是 AsyncSequence,可以用 for try await
  • outboundStream 可以 await write
  • 自动处理 EventLoop context

Q: 如何在 swift-nio 版本 < 2.40 时使用 async?

A: 用 continuation 桥接:

extension EventLoopFuture {
    func get() async throws -> Value {
        try await withCheckedThrowingContinuation { continuation in
            whenComplete { continuation.resume(with: $0) }
        }
    }
}

SwiftNIO 2.40+ 内置了 .get() 方法,支持 async。

小结

本章你学会了 SwiftNIO 与 Swift async/await 的集成:

  • Future → async 桥接:withCheckedContinuation 把回调式转 async
  • NIOLoopBoundBox:跨 Actor 安全访问 EventLoop-bound 值
  • NIOAsyncChannel:SwiftNIO 2.40+ 的原生 async/await API
  • 避免阻塞:Task.sleep vs Thread.sleep 的关键区别
  • Sendable 约束:为什么 Channel 不符合 Sendable,如何正确包装

现代 Swift 项目应该优先使用 async/await,SwiftNIO 提供的桥接方式让你不必放弃 SwiftNIO 的网络能力。

术语表

中文英文说明
ContinuationContinuationasync/await 的底层挂起/恢复机制
EventLoop 绑定EventLoop-bound值绑定到特定 EventLoop,只能在其上操作
Actor 隔离Actor isolationActor 保护内部状态,外部需 Sendable
SendableSendable可跨并发边界安全传递的类型
桥接Bridging两种异步模型的连接方式
阻塞Blocking等待操作完成,暂停线程
非阻塞Non-blocking不等待,立即返回或挂起

知识检查

  1. 为什么不能在 EventLoop 线程调用 future.wait()

  2. NIOLoopBoundBox 如何保证跨 Actor 安全访问 Channel?

  3. NIOAsyncChannel 相比传统 Channel 有什么优势?

点击查看答案与解析
  1. wait() 会阻塞整个 EventLoop:EventLoop 是单线程,管理数千连接。调用 wait() 时,线程停在原地等待,所有其他连接的处理都被卡住。正确做法是用 continuation 桥接成 async,或用回调 .whenComplete {},不阻塞线程。

  2. NIOLoopBoundBox 记录 EventLoop 并提供 withValue {}:它包装 Channel 并记录绑定的 EventLoop。调用 withValue 时,如果当前不在正确的 EventLoop 上,会自动提交任务到该 EventLoop。这保证 Channel 只在其 EventLoop 上被修改,满足 Sendable 约束。

  3. NIOAsyncChannel 提供原生 async/await 接口

    • inboundStream 是 AsyncSequence,用 for try await 读取
    • 不需要手动处理 EventLoopFuture 或 continuation
    • 自动继承 EventLoop context,避免 context 丢失
    • 代码更简洁,符合现代 Swift 风格

继续学习

下一章: 系统编程与进程管理 - 学习 Process、Signal、跨平台系统调用

返回: 高级进阶概览

系统编程与进程管理

开篇故事

想象你是一家餐厅的经理。除了管理厨房,你还需要协调外卖平台、供应商、清洁服务。你不是亲自做菜,而是"调度"各种外部服务。

系统编程就是这种"经理角色"。你的 Swift 程序是核心业务,但你需要:

  • 调用 git 命令获取代码版本
  • 启动 npm 构建前端资源
  • 执行 shell 脚本处理数据文件
  • 监听 Ctrl+C 信号优雅关闭

本章教你的,就是如何在 Swift 里当好这个"经理"——执行外部命令、管理进程、处理信号,同时保证 macOS 和 Linux 双平台兼容。

本章适合谁

如果你满足以下任一情况,这一章就是为你准备的:

  • 你需要构建 CLI 工具,调用外部命令(如 git、docker、npm)
  • 你想让程序优雅关闭,响应 Ctrl+C (SIGINT)
  • 你关心跨平台部署,想让代码跑在 macOS 和 Linux
  • 你想知道 Process、Signal、POSIX 这些概念怎么用

你会学到什么

完成本章后,你将掌握以下内容:

  • Process 类:Foundation 的进程执行 API,捕获 stdout/stderr
  • 信号处理:SIGINT/SIGTERM 捕获,优雅关闭模式
  • 跨平台路径:macOS Documents/Caches vs Linux /tmp/home
  • ProcessInfo:系统信息、环境变量、平台检测
  • 超时处理:避免进程无限等待的实用模式

前置要求

在开始之前,请确保你已掌握以下内容:

  • Swift 基础:do-catch 错误处理、可选类型
  • 基础命令行知识:知道 ls、echo、pwd 等命令
  • Foundation 基础:FileManager、Pipe 的基本概念

运行环境要求:

  • macOS 12.0+ 或 Linux(Ubuntu 22.04+)
  • Swift 6.0+
  • 基础 shell 命令(ls、echo、pwd)

第一个例子

先看一个最基础的例子:执行 /bin/ls 命令,列出当前目录。

这段代码来自 AdvanceSample/Sources/AdvanceSample/ProcessSample.swift

import Foundation

// 创建 Process 实例
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/ls")
process.arguments = ["-la"]  // 参数列表

// 执行并等待完成
do {
    process.run()
    process.waitUntilExit()
    
    if process.terminationStatus == 0 {
        print("命令执行成功")
    } else {
        print("命令失败,状态码: \(process.terminationStatus)")
    }
} catch {
    print("执行错误: \(error)")
}

运行这段代码,输出类似:

命令执行成功

原理解析

Process:进程执行的核心类

Foundation 的 Process 类封装了进程创建和管理的全过程:

关键属性

  • executableURL:可执行文件路径(URL 类型)
  • arguments:命令参数数组 [String]
  • standardOutput:stdout 输出管道(Pipe)
  • standardError:stderr 输出管道(Pipe)
  • terminationStatus:进程退出码(0 成功,非 0 失败)

关键方法

  • run():启动进程(非阻塞)
  • waitUntilExit():等待进程完成(阻塞)
  • terminate():强制终止进程(SIGKILL)

捕获 stdout/stderr

默认情况下,子进程继承父进程的 stdout/stderr。要捕获输出,需要用 Pipe

let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/echo")
process.arguments = ["Hello from Process"]

// 创建输出管道
let stdoutPipe = Pipe()
process.standardOutput = stdoutPipe

// 创建错误管道
let stderrPipe = Pipe()
process.standardError = stderrPipe

process.run()
process.waitUntilExit()

// 读取输出
let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
let stdout = String(data: stdoutData, encoding: .utf8) ?? ""

print("输出: \(stdout)")  // "输出: Hello from Process"

进程超时处理

有些命令可能卡住(如网络请求失败)。用超时机制保护:

func executeWithTimeout(command: String, args: [String], timeout: TimeInterval) -> Bool {
    let process = Process()
    process.executableURL = URL(fileURLWithPath: command)
    process.arguments = args
    
    process.run()
    
    let startTime = Date()
    while process.isRunning && Date().timeIntervalSince(startTime) < timeout {
        Thread.sleep(forTimeInterval: 0.1)
    }
    
    if process.isRunning {
        print("超时,终止进程")
        process.terminate()
        return false
    }
    
    return process.terminationStatus == 0
}

跨平台路径差异

macOS 和 Linux 的目录结构不同:

目录类型macOSLinux
Documents~/Documents无(需手动创建)
Caches~/Library/Caches/tmp 或 ~/.cache
Application Support~/Library/Application Support~/.config 或 ~/.local/share
Temporary/tmp 或 NSTemporaryDirectory()/tmp
CurrentFileManager.currentDirectoryPath相同

跨平台建议

  • FileManager.currentDirectoryPath(所有平台可用)
  • FileManager.temporaryDirectory(所有平台可用)
  • ProcessInfo.processInfo.environment["HOME"](所有平台可用)
  • 避免硬编码 /Users/.../home/...
func getPlatformPath() -> String {
    let fileManager = FileManager.default
    
    #if os(macOS)
    // macOS 有标准沙箱路径
    if let documents = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
        return documents.path
    }
    #elseif os(Linux)
    // Linux 用 HOME 或当前目录
    let home = ProcessInfo.processInfo.environment["HOME"] ?? "/tmp"
    return home
    #endif
    
    // 默认:当前目录
    return fileManager.currentDirectoryPath
}

Signal 处理(概念)

Signal 是操作系统发送给进程的"通知"。常见信号:

Signal编号含义可捕获
SIGINT2Ctrl+C 中断
SIGTERM15优雅终止请求
SIGHUP1终端挂起
SIGKILL9强制终止

Foundation 的 DispatchSourceSignal

import Dispatch

// 监听 SIGINT(Ctrl+C)
let sigintSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)

// 阻止默认行为(立即退出)
signal(SIGINT, SIG_IGN)

sigintSource.setEventHandler {
    print("收到 SIGINT,准备优雅关闭...")
    // 执行清理逻辑
    cleanupResources()
    exit(0)
}

sigintSource.resume()

常见错误

错误原因解决方案
executableURL not found命令路径不存在/usr/bin/which 查找实际路径
Process.run() failed权限不足或命令无效检查 executableURL 是否可执行
Pipe read blocks未调用 waitUntilExit 或进程卡住加超时机制
Cross-platform path missingLinux 无 Documents 目录用 currentDirectoryPath 替代
Signal handler crashHandler 中执行复杂操作Handler 应只设置标志,主循环处理

错误示例 1:路径不存在

// ❌ 错误 - 可能不存在
process.executableURL = URL(fileURLWithPath: "git")

// ✅ 正确 - 使用绝对路径
process.executableURL = URL(fileURLWithPath: "/usr/bin/git")

错误示例 2:阻塞读取

// ❌ 错误 - 进程可能卡住
process.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()  // 无限等待

// ✅ 正确 - 加超时
process.run()
let deadline = Date() + 5.0  // 5秒超时
while process.isRunning && Date() < deadline {
    Thread.sleep(forTimeInterval: 0.1)
}
if process.isRunning {
    process.terminate()
}

错误示例 3:Signal Handler 复杂操作

// ❌ 错误 - Handler 中执行耗时操作
sigintSource.setEventHandler {
    saveLargeFile()  // 可能耗时几秒
    exit(0)
}

// ✅ 正确 - Handler 只设置标志
var shouldExit = false
sigintSource.setEventHandler {
    shouldExit = true  // 只设置标志
}

// 主循环检查标志
while !shouldExit {
    // 正常工作...
}
cleanupResources()
exit(0)

Swift vs Rust/Python 对比

概念Swift (Foundation)Rust (std::process)Python (subprocess)
进程执行ProcessCommandsubprocess.run
输出捕获PipeStdio::pipedcapture_output=True
参数传递arguments: [String].args([...])args=[...]
状态码terminationStatus.status.code().returncode
超时控制手动循环检查.timeout()timeout=N
SignalDispatchSourceSignalctrlc cratesignal.signal()
跨平台路径FileManager + ProcessInfostd::envos.path

关键差异

  • Swift 的 Process 超时需手动实现(循环检查 isRunning)
  • Rust 的 Command 提供 .timeout() 方法(更优雅)
  • Python 的 subprocess.run 有 timeout= 参数(最简洁)

动手练习 Level 1

任务:写一个函数执行 /usr/bin/git --version,捕获并打印输出。

要求:

  1. 使用 Process 和 Pipe
  2. 打印 stdout 内容
  3. 打印 terminationStatus
点击查看参考答案
func getGitVersion() {
    let process = Process()
    process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
    process.arguments = ["--version"]
    
    let stdoutPipe = Pipe()
    process.standardOutput = stdoutPipe
    
    do {
        process.run()
        process.waitUntilExit()
        
        let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(data: data, encoding: .utf8) ?? ""
        
        print("Git 版本: \(output.trimmingCharacters(in: .whitespacesAndNewlines))")
        print("状态码: \(process.terminationStatus)")
    } catch {
        print("错误: \(error)")
    }
}

动手练习 Level 2

任务:写一个跨平台的"获取用户目录"函数。

要求:

  1. macOS 返回 Documents 目录
  2. Linux 返回 HOME 目录
  3. 使用 #if os() 编译条件
点击查看参考答案
func getUserDirectory() -> String {
    let fileManager = FileManager.default
    
    #if os(macOS)
    if let documents = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
        return documents.path
    }
    #elseif os(Linux)
    if let home = ProcessInfo.processInfo.environment["HOME"] {
        return home
    }
    #endif
    
    // 默认回退
    return fileManager.currentDirectoryPath
}

动手练习 Level 3

任务:实现一个带超时的 Process 执行器。

要求:

  1. 函数签名:func execute(command: String, args: [String], timeout: TimeInterval) -> (stdout: String, success: Bool)
  2. 超时后自动 terminate()
  3. 返回 stdout 内容和成功状态
点击查看参考答案
func execute(command: String, args: [String], timeout: TimeInterval) -> (stdout: String, success: Bool) {
    let process = Process()
    process.executableURL = URL(fileURLWithPath: command)
    process.arguments = args
    
    let stdoutPipe = Pipe()
    process.standardOutput = stdoutPipe
    
    process.run()
    
    let startTime = Date()
    while process.isRunning && Date().timeIntervalSince(startTime) < timeout {
        Thread.sleep(forTimeInterval: 0.1)
    }
    
    if process.isRunning {
        process.terminate()
        process.waitUntilExit()
        return ("", false)
    }
    
    let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
    let stdout = String(data: data, encoding: .utf8) ?? ""
    
    return (stdout, process.terminationStatus == 0)
}

故障排查 FAQ

Q: Process.run() 报错 "The file doesn't exist"

A: 命令路径错误。用终端 which 命令查找实际路径:

which git
# 输出: /usr/bin/git

Swift 中使用该路径。


Q: Pipe readDataToEndOfFile 无限阻塞

A: 进程没有退出或输出量很大。解决方案:

  1. 确保 waitUntilExit() 被调用
  2. 加超时机制(见 Level 3 练习)
  3. readData(ofLength:) 分批读取

Q: Linux 上 Documents 目录不存在

A: Linux 没有 macOS 的沙箱目录。使用替代:

#if os(Linux)
let home = ProcessInfo.processInfo.environment["HOME"] ?? "/tmp"
let documents = home + "/Documents"  // 手动创建
FileManager.default.createDirectory(atPath: documents, withIntermediateDirectories: true)
#endif

Q: Signal Handler 不生效

A: 检查两点:

  1. 调用 signal(SIGINT, SIG_IGN) 阻止默认行为
  2. Handler 在正确的队列上设置(通常 .main

Q: Process.terminate() 后进程仍在运行

A: terminate() 发送 SIGTERM,进程可能忽略。用 kill() 强制终止:

if process.isRunning {
    process.terminate()
    Thread.sleep(forTimeInterval: 1.0)
    if process.isRunning {
        process.kill()  // SIGKILL,无法忽略
    }
}

小结

本章你学会了系统编程的核心技能:

  • Process 执行:Foundation API、参数传递、状态检查
  • 输出捕获:Pipe、stdout/stderr 分离
  • 超时控制:循环检查 isRunning、自动 terminate
  • 跨平台路径:macOS vs Linux 的目录差异
  • Signal 处理:SIGINT/SIGTERM 捕获、优雅关闭模式

系统编程是 CLI 工具的基础能力。掌握 Process 和 Signal,你就能构建生产级的命令行应用。

术语表

中文英文说明
进程Process运行中的程序实例
子进程Child process由父进程启动的进程
状态码Termination status进程退出返回的数值(0 成功)
管道Pipe进程间通信的数据流
SignalSignal操作系统发送给进程的通知
捕获Catch接收并处理 Signal
优雅关闭Graceful shutdown先清理资源再退出
沙箱SandboxmacOS 的应用隔离目录

知识检查

  1. Process 的 waitUntilExit()terminate() 有什么区别?

  2. 为什么 Linux 没有 Documents/Caches 目录?

  3. Signal Handler 中应该避免什么操作?

点击查看答案与解析
  1. waitUntilExit() 是等待,terminate() 是终止

    • waitUntilExit() 阻塞当前线程,等待子进程自然结束
    • terminate() 立即向子进程发送 SIGTERM,请求终止
    • 两者独立:terminate() 后仍需 waitUntilExit() 确认退出
  2. Linux 没有 macOS 的沙箱机制

    • macOS 应用运行在沙箱中,有固定的 Documents/Caches/Application Support 目录
    • Linux 是传统文件系统,只有 /tmp、/home/user、当前目录等通用路径
    • 跨平台代码应避免依赖沙箱路径,用 currentDirectoryPath 或 HOME 替代
  3. Signal Handler 中避免复杂操作

    • Signal Handler 在中断上下文执行,不是正常线程环境
    • 执行耗时操作(如文件保存、网络请求)可能导致死锁或崩溃
    • 正确做法:只设置标志变量,主循环检测标志后执行清理

继续学习

下一章: 测试框架与质量保证 - 学习 XCTest、异步测试、性能基准

返回: 高级进阶概览

测试框架与质量保证

开篇故事

你刚装修完房子,装修公司说"质量没问题"。但你心里犯嘀咕:水管真的不漏水?电路真的安全?门窗真的密封?

于是你决定自己测试:

  • 打开水龙头,看水流是否正常
  • 开关插座,看电路是否稳定
  • 关上窗户,听外面噪音是否隔绝

软件开发也需要这种"自己测试"的精神。测试框架就是你的"验收工具"——运行代码、检查结果、发现问题。

本章教你的,是 Swift 内置的 XCTest 框架。它不需要额外安装,swift test 就能运行。学会它,你就能为自己的代码写"验收报告"。

本章适合谁

如果你满足以下任一情况,这一章就是为你准备的:

  • 你想学会写单元测试,但不知道从哪开始
  • 你听说 XCTest 但不确定怎么用 async 测试
  • 你想知道 XCTAssertEqual、XCTAssertTrue 这些断言怎么用
  • 你想让代码更可靠,减少"改一个地方,崩三个功能"的噩梦

你会学到什么

完成本章后,你将掌握以下内容:

  • XCTestCase:测试类的基本结构,setUp/tearDown 生命周期
  • 断言方法:XCTAssertEqual、XCTAssertTrue、XCTAssertThrowsError 等
  • 异步测试:async test 方法,await 在测试中的用法
  • 性能测试:measure {} 测量代码执行时间
  • 运行测试:swift test、--filter、测试报告解读

前置要求

在开始之前,请确保你已掌握以下内容:

  • Swift 基础:函数、类、可选类型
  • 错误处理:do-catch-try 模式
  • async/await:基本异步函数用法

运行环境要求:

  • macOS 12.0+ 或 Linux
  • Swift 6.0+
  • 测试文件位于 Tests/<TargetName>Tests/

第一个例子

先看一个最基础的测试:验证字符串拼接是否正确。

这段代码来自 AdvanceSample/Tests/AdvanceSampleTests/TestingSampleTests.swift

import XCTest
@testable import AdvanceSample

final class StringTests: XCTestCase {
    
    func testStringConcatenation() {
        let first = "Hello"
        let second = "World"
        let result = first + " " + second
        
        XCTAssertEqual(result, "Hello World")
    }
}

运行测试:

swift test --filter StringTests

输出:

Test Case 'StringTests.testStringConcatenation' passed (0.001 seconds)

原理解析

XCTestCase:测试的容器

所有测试类继承 XCTestCase

import XCTest

final class MyTests: XCTestCase {
    // 每个测试方法以 func test... 命名
    func testSomething() {
        // 测试代码...
    }
}

关键约定

  • 类名通常以 Tests 结尾(如 StringTests
  • 测试方法名必须以 test 开头(如 testStringConcatenation()
  • 测试方法无参数、无返回值

断言方法一览

XCTest 提供丰富的断言方法:

断言用途示例
XCTAssertEqual比较两个值相等XCTAssertEqual(1 + 1, 2)
XCTAssertNotEqual比较两个值不等XCTAssertNotEqual(1, 2)
XCTAssertTrue验证条件为 trueXCTAssertTrue(5 > 3)
XCTAssertFalse验证条件为 falseXCTAssertFalse(1 > 2)
XCTAssertNil验证值为 nilXCTAssertNil(optionalValue)
XCTAssertNotNil验证值不为 nilXCTAssertNotNil(optionalValue)
XCTAssertGreaterThan验证大于XCTAssertGreaterThan(5, 3)
XCTAssertLessThan验证小于XCTAssertLessThan(1, 5)
XCTAssertThrowsError验证抛出错误XCTAssertThrowsError(try throwIfNegative(-1))
XCTAssertNoThrow验证不抛出错误XCTAssertNoThrow(try safeOperation())

setUp/tearDown:测试生命周期

每个测试方法独立运行。如果需要共享初始化逻辑,用 setUp/tearDown:

final class DatabaseTests: XCTestCase {
    
    var database: Database!
    
    // 每个测试前执行
    override func setUp() {
        super.setUp()
        database = Database()
        database.connect()
    }
    
    // 每个测试后执行
    override func tearDown() {
        database.disconnect()
        database = nil
        super.tearDown()
    }
    
    func testInsert() {
        database.insert(record: "test")
        XCTAssertEqual(database.count, 1)
    }
    
    func testDelete() {
        database.insert(record: "test")
        database.delete(record: "test")
        XCTAssertEqual(database.count, 0)
    }
}

执行顺序

setUp() → testInsert() → tearDown()
setUp() → testDelete() → tearDown()

每个测试前后都执行 setUp/tearDown,保证测试隔离。

异步测试(async test)

Swift 6.0 的 XCTest 支持 async 测试方法:

final class AsyncTests: XCTestCase {
    
    // async 测试方法
    func testAsyncOperation() async throws {
        let result = await performAsyncWork()
        XCTAssertEqual(result, "completed")
    }
    
    // async + throwing 测试方法
    func testAsyncThrowing() async throws {
        let value = try await fetchFromNetwork()
        XCTAssertGreaterThan(value, 0)
    }
}

关键要点

  • 测试方法标记 async(可选加 throws
  • 可以用 await 调用 async 函数
  • 不需要手动等待,测试框架自动处理

性能测试(measure)

测量代码执行时间:

func testPerformance() {
    measure {
        // 被测量的代码
        let _ = calculateSum()
    }
}

func calculateSum() -> Int {
    var sum = 0
    for i in 0..<10000 {
        sum += i
    }
    return sum
}

输出类似:

Test Case 'AsyncTests.testPerformance' measured [Time, seconds] average: 0.002

measure 会多次运行代码块,计算平均时间。

@testable import

测试文件需要导入被测试模块:

@testable import AdvanceSample  // 可以访问 internal 成员

@testable 让测试可以访问 internal 访问级别的成员,而不是只有 public

常见错误

错误原因解决方案
Test method not found方法名不以 test 开头重命名为 test...
Module not found未导入被测试模块添加 @testable import
Async test hangsawait 死锁或超时加 XCTWaiter 或 timeout
setUp crash初始化失败检查 setUp 中的依赖
Assertion failed测试条件不满足检查预期值与实际值

错误示例 1:方法命名错误

// ❌ 错误 - 方法名不以 test 开头
func checkString() {
    XCTAssertEqual("a", "a")
}

// ✅ 正确
func testStringCheck() {
    XCTAssertEqual("a", "a")
}

错误示例 2:未导入模块

// ❌ 错误 - 编译器找不到 AdvanceSample
final class MyTests: XCTestCase {
    func testFunction() {
        let result = advanceSample()  // Error: use of unresolved identifier
    }
}

// ✅ 正确
@testable import AdvanceSample

final class MyTests: XCTestCase {
    func testFunction() {
        let result = advanceSample()  // OK
    }
}

错误示例 3:异步死锁

// ❌ 错误 - 异步任务卡住
func testAsyncBad() async {
    let future = someOperation()
    // 如果 future 永不完成,测试卡住
    let result = await future.value
}

// ✅ 正确 - 加超时检查
func testAsyncGood() async throws {
    let future = someOperation()
    
    // 使用 Task.sleep 模拟超时检测
    try await withTimeout(seconds: 5) {
        let result = await future.value
        XCTAssertEqual(result, expected)
    }
}

Swift vs Rust/Python 对比

概念Swift (XCTest)Rust (cargo test)Python (pytest)
测试框架XCTestBuilt-in + #[test]pytest (第三方)
测试类XCTestCase自由函数自由函数或类
断言XCTAssertEqual 等assert_eq! 等assert 等
异步测试async functokio::testasyncio
性能测试measure {}criterion (第三方)pytest-benchmark
运行命令swift testcargo testpytest
过滤测试--filter Name--test name-k name

关键差异

  • Swift XCTest 是 Apple 官方框架,macOS/Linux 通用
  • Rust 测试内置在 cargo,无需额外依赖
  • Python pytest 是第三方,功能更丰富(fixtures、parametrize)

动手练习 Level 1

任务:为以下函数写测试。

func add(a: Int, b: Int) -> Int {
    return a + b
}

func isEven(number: Int) -> Bool {
    return number % 2 == 0
}

要求:

  1. 创建 MathTests: XCTestCase
  2. testAdd() 验证 1+1=2
  3. testIsEven() 验证偶数判断
点击查看参考答案
import XCTest
@testable import AdvanceSample

final class MathTests: XCTestCase {
    
    func testAdd() {
        XCTAssertEqual(add(1, 1), 2)
        XCTAssertEqual(add(-1, 1), 0)
        XCTAssertEqual(add(0, 0), 0)
    }
    
    func testIsEven() {
        XCTAssertTrue(isEven(2))
        XCTAssertTrue(isEven(0))
        XCTAssertFalse(isEven(1))
        XCTAssertFalse(isEven(-3))
    }
}

动手练习 Level 2

任务:写一个异步测试,验证 async 函数返回值。

func fetchUserID() async -> Int {
    await Task.sleep(nanoseconds: 100_000_000)  // 100ms
    return 42
}

要求:

  1. 测试方法标记 async
  2. await 调用 fetchUserID()
  3. 验证返回值是 42
点击查看参考答案
func testFetchUserID() async {
    let userId = await fetchUserID()
    XCTAssertEqual(userId, 42)
}

动手练习 Level 3

任务:写一个性能测试,测量字符串拼接时间。

要求:

  1. 使用 measure {}
  2. 拼接 1000 个字符串
  3. 检查输出中的平均时间
点击查看参考答案
func testStringConcatenationPerformance() {
    measure {
        var result = ""
        for i in 0..<1000 {
            result += "item\(i)"
        }
    }
}

故障排查 FAQ

Q: swift test 报错 "No such module 'AdvanceSample'"

A: 检查 Package.swift 的 testTarget 配置:

.testTarget(
    name: "AdvanceSampleTests",
    dependencies: ["AdvanceSample"]  // 必须声明依赖
),

Q: 测试方法不被识别

A: 确认三点:

  1. 类继承 XCTestCase
  2. 方法名以 test 开头
  3. 方法无参数、无返回值

Q: 异步测试超时卡住

A: XCTest 异步测试默认无超时。用 XCTWaiter 或手动检测:

func testWithTimeout() async throws {
    let waiter = XCTWaiter(delegate: self)
    let expectation = XCTestExpectation(description: "Async complete")
    
    Task {
        await someAsyncWork()
        expectation.fulfill()
    }
    
    waiter.wait(for: [expectation], timeout: 5.0)
}

Q: setUp 中创建的资源在测试后未清理

A: 确保 tearDown 正确实现。如果 setUp 抛出异常,tearDown 仍会执行:

override func setUpWithError() throws {
    // throwing setUp
}

override func tearDownWithError() throws {
    // throwing tearDown(即使 setUp 失败也会执行)
}

Q: 如何只运行部分测试?

A: 用 --filter 参数:

swift test --filter MathTests  # 只运行 MathTests 类
swift test --filter testAdd    # 只运行 testAdd 方法

小结

本章你学会了 XCTest 的核心用法:

  • XCTestCase:测试类结构、命名约定
  • 断言方法:Equal/True/Nil/ThrowsError 等断言
  • setUp/tearDown:测试生命周期管理
  • async 测试:async func + await 的测试方式
  • 性能测试:measure {} 测量执行时间
  • 运行测试:swift test、--filter 过滤

测试是现代软件开发的必备技能。XCTest 虽然功能相对简单,但它内置在 Swift 工具链,无需额外安装,适合快速建立测试习惯。

术语表

中文英文说明
测试类XCTestCase包含多个测试方法的类
测试方法Test method以 test 开头的函数
断言Assertion检查预期结果的语句
生命周期LifecyclesetUp → test → tearDown 的执行顺序
异步测试Async test标记 async 的测试方法
性能测试Performance testmeasure {} 测量执行时间
测试隔离Test isolation每个测试独立运行,不影响其他
测试过滤Test filter--filter 只运行部分测试

知识检查

  1. XCTestCase 的 setUp 和 tearDown 在什么时候执行?

  2. XCTAssertEqual 和 XCTAssertTrue 有什么区别?

  3. 为什么测试方法名必须以 test 开头?

点击查看答案与解析
  1. setUp 在每个测试方法前执行,tearDown 在每个测试方法后执行

    • 执行顺序:setUp → testMethod1 → tearDown → setUp → testMethod2 → tearDown
    • 每个测试前后都会调用,保证测试隔离
    • setUp 用于初始化共享资源,tearDown 用于清理
  2. XCTAssertEqual 比较两个值相等,XCTAssertTrue 检查条件为真

    • XCTAssertEqual(1+1, 2) 检查 1+1 是否等于 2(精确值比较)
    • XCTAssertTrue(5>3) 检查 5>3 是否为真(布尔条件)
    • XCTAssertEqual 适用于比较具体值,XCTAssertTrue 适用于逻辑条件
  3. XCTest 通过方法名前缀识别测试方法

    • XCTest 运行时扫描所有 XCTestCase 子类
    • 找到以 test 开头的方法,自动执行
    • 非 test 开头的方法被忽略,不作为测试运行
    • 这是 XCTest 的约定,便于框架自动发现测试

继续学习

下一章: 阶段复习:高级部分 - 综合测试高级部分知识

返回: 高级进阶概览

🌐 Vapor Web 框架

开篇故事

想象你要开一家餐厅。你可以自己砌砖盖房、布置厨房、设计菜单、雇服务员——这需要几个月时间。或者,你可以直接租一个装修完毕的商用厨房,里面炉灶、冰箱、排风系统一应俱全,你只需要专注于做菜和定菜单。

Vapor 就是 Swift 世界里的那个"商用厨房"。

HTTP 请求像源源不断的订单,路由(Routes)是前台服务员,把每张单子送到对应的厨师手里。中间件(Middleware)是厨房里的质检员,检查食材新鲜度、记录每道菜的制作日志。Content 协议是标准化餐具规格,确保每个盘子都能放进洗碗机。你不需要操心底层 TCP 连接怎么处理、HTTP 报文怎么解析——Vapor 全都帮你搞定了。

本章要教你的,就是如何在 Swift 里用 Vapor 快速搭建一个 RESTful API 服务。

本章适合谁

如果你满足以下任一情况,这一章就是为你准备的:

  • 你想用 Swift 编写 Web API 后端服务,而不是只写 iOS/macOS 客户端
  • 你听过 Vapor 这个名字,但不清楚它和 Spring Boot / Express.js 有什么区别
  • 你需要了解路由定义、JSON 请求/响应体处理、中间件链路这些 Web 框架核心概念
  • 你想看看 Swift 在服务器端开发中的实际表现

本章面向已经掌握 Swift 基础语法并了解异步编程(async/await)的开发者。如果你还没有接触过并发编程,建议先回顾 并发编程 章节。

你会学到什么

完成本章后,你将掌握以下内容:

  • 路由定义(Routes):GET / POST 路由的声明方式,路径参数提取(Path Parameter)
  • Content 协议:如何用 Content protocol 声明请求/响应模型,实现 JSON 自动编解码
  • 中间件(Middleware)AsyncMiddleware 协议的工作原理,日志中间件的编写方式

前置要求

在开始之前,请确保你已掌握以下内容:

  • Swift 6.0 语法:结构体声明、协议遵守(Protocol Conformance)、属性定义
  • 异步编程:async/awaittry/throw 的错误传递
  • JSON 处理:理解 Codable 协议的工作原理(参考 JSON 处理
  • macOS 12+ 或 Linux (Ubuntu 22.04+) 运行环境

如果你对这些内容还不太熟悉,建议先回顾基础部分(协议 → 并发编程 → JSON 处理),然后再回来。

第一个例子

我们先来看一个最基础的例子。目标很明确:创建一个 Vapor 应用,定义一个 GET 路由,当访问 /api/hello 时返回一段字符串。

这段代码来自 AdvanceSample/Sources/AdvanceSample/VaporSample.swift 第 1 到 11 行。

import Vapor

let app = Application(.testing)
defer { app.shutdown() }

app.get("api", "hello") { req -> String in
    return "Hello from Vapor!"
}

三件事:创建 Application 实例、注册一个 GET 路由、处理返回字符串。注意 defer { app.shutdown() } —— 这确保应用退出时资源被正确释放。

在真实项目中,你通常会用 .production.development 环境,而不是 .testing。这里的 .testing 让 Vapor 在后台静默运行,不占用终端端口,非常适合示例代码演示。

如果想让服务真正监听端口,可以这样启动:

try app.start()
print("Vapor server started on http://localhost:\(app.http.server.address?.port ?? 8080)")

// 运行一段时间后优雅关闭
try await app.asyncShutdown()

原理解析

Vapor 是一个异步 Web 框架,底层基于 SwiftNIO 构建。理解它的核心组件,你就能举一反三。

Application

Application 是整个 Vapor 应用的入口和生命周期管理器。它持有 HTTP 服务器、路由注册表、中间件链、服务容器等一切资源。

let app = Application(.testing)
defer { app.shutdown() }

创建后必须调用 start() 启动服务,程序退出前必须调用 shutdown() 释放资源。defer 是确保即使中间抛出异常,shutdown 也会被执行。

Routes(路由)

路由的作用是将 HTTP 请求映射到处理函数。Vapor 用路径片段(Path Segments)声明路由:

// GET /api/hello
app.get("api", "hello") { req -> String in
    return "Hello from Vapor!"
}

// GET /api/users/:id
app.get("api", "users", ":id") { req -> String in
    guard let id = req.parameters.get("id") else {
        throw Abort(.badRequest, reason: "Missing user ID")
    }
    return "User ID: \(id)"
}

:id 是路径参数(Path Parameter),通过 req.parameters.get("id") 提取。如果参数缺失,抛出 Abort(.badRequest) 会直接返回 400 状态码。

Content Protocol(内容协议)

HTTP POST 请求通常携带 JSON 请求体。Vapor 用 Content 协议实现自动编解码。你需要定义遵守 Content 的结构体:

struct UserRequest: Content {
    let name: String
    let email: String
}

struct UserResponse: Content {
    let id: String
    let name: String
    let email: String
}

// POST /api/users
app.post("api", "users") { req -> UserResponse in
    let userReq = try req.content.decode(UserRequest.self)
    return UserResponse(id: UUID().uuidString, name: userReq.name, email: userReq.email)
}

Content 协议自动继承了 Codable,所以解码逻辑和 JSON 章节学到的 Codable 完全一致。req.content.decode(Type.self) 会把 HTTP Body 的 JSON 解码成你的模型。返回值是 UserResponse 的话,Vapor 会自动把它编码成 JSON 作为 HTTP Response Body 返回。

Middleware(中间件)

中间件是请求处理链上的拦截器。它在请求到达路由处理函数之前之后执行自定义逻辑。典型的中间件场景:日志记录、认证校验、CORS 处理、限流。

struct LoggingMiddleware: AsyncMiddleware {
    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        print("[Middleware] \(request.method.rawValue) \(request.url.path)")
        let response = try await next.respond(to: request)
        print("[Middleware] Response: \(response.status.code)")
        return response
    }
}

AsyncMiddleware 协议要求实现 respond(to:chainingTo:) 方法。关键理解点:next.respond(to: request) 才会把请求传递给链路中的下一个环节(可能是另一个中间件,也可能是最终的路由处理函数)。在这行代码之前,你可以做任何前置处理;在这行之后,你可以查看或修改最终的 Response。

使用方法:

// 将中间件应用到特定路由组
app.grouped(LoggingMiddleware()).get("api", "protected") { req -> String in
    return "Protected resource"
}

grouped() 创建一个路由组,组内的所有请求都会经过 LoggingMiddleware 处理。这是细粒度中间件绑定方式。你也可以用 app.middleware.use(LoggingMiddleware()) 将中间件注册为全局中间件。

常见错误

以下是最容易踩到的三个坑。

错误症状原因修复方式
路由冲突(409 Route Collision)Vapor.RouteRegistrationError两条路由注册了相同的方法+路径检查 app.get/post 路径是否有重复
缺少 Content 声明编译报错:does not conform to Content自定义结构体没有遵守 Content 协议在结构体声明后加 : Content
中间件顺序错误认证中间件没有执行注册中间件的顺序不对全局中间件按 app.middleware.use() 的调用顺序排列,先 auth 后 log
路径参数未提取路由匹配了但拿到 nil路径声明写了 :idreq.parameters.get() 用了别的名字确保 parameters.get("id") 和路径段 ":id" 名称一致
忘记 shutdown测试跑完后进程不退出、端口一直被占用创建 Application 后没有 defer { app.shutdown() }使用 defer 块保证资源释放

Swift vs Rust/Python 对比

不同的服务端语言有不同的 Web 框架。放在一起对比会更直观:

特性Swift (Vapor)Rust (Axum)Python (FastAPI)
应用启动Application(.testing).start()axum::serve(listener, app).awaituvicorn.run(app)
GET 路由app.get("api", "hello") { ... }Router::get("/api/hello", handler)@app.get("/api/hello") async def ...
路径参数req.parameters.get("id")Path(id): Path<String>async def func(id: str)
JSON Bodytry req.content.decode(UserRequest.self)Json(user): Json<UserRequest>user: UserRequest
请求/响应模型struct: Content (=Codable)struct: Serialize + Deserializeclass: BaseModel (Pydantic)
中间件AsyncMiddleware 协议tower::Layer 组合式@app.middleware("http")
异步模型Swift Concurrency (async/await)Tokio 运行时 + asyncasyncio
底层引擎SwiftNIO (非阻塞 I/O)hyper / tokiouvicorn (ASGI)
类型安全编译期检查编译期检查运行期检查

Swift 的 Vapor 和 Rust 的 Axum 在设计理念上高度一致:路由声明 + 类型安全请求/响应模型 + 组合式中间件。Python 的 FastAPI 更倾向于用装饰器语法,开发速度快但缺少编译期保证。

动手练习 Level 1

目标:添加一个 DELETE 端点。

现在你已经有了 GET /api/hello、GET /api/users/:id 和 POST /api/users。你的任务是添加一个 DELETE /api/users/:id 端点:

  1. app 上注册 DELETE 路由:app.delete("api", "users", ":id")
  2. 提取路径参数 id
  3. 返回字符串 "Deleted user: \(id)"
  4. 如果 id 缺失,抛出 Abort(.badRequest, reason: "Missing user ID")
点击查看答案
app.delete("api", "users", ":id") { req -> String in
    guard let id = req.parameters.get("id") else {
        throw Abort(.badRequest, reason: "Missing user ID")
    }
    return "Deleted user: \(id)"
}

注意:delete 方法和 get/post 的调用方式完全一致,只是 HTTP Method 不同。Vapor 会为每种 HTTP Method 提供对应的方法名。

动手练习 Level 2

目标:添加请求验证中间件。

在 POST /api/users 之前,你需要验证请求体中 name 字段非空且 email 包含 @。编写一个 ValidationMiddleware

  1. 创建一个结构体 ValidationMiddleware,遵守 AsyncMiddleware
  2. respond(to:chainingTo:) 方法中,先检查请求方法是否为 POST
  3. 对 POST 请求,打印 [Validation] Checking request body...
  4. 调用 next.respond(to: request) 继续链路
  5. 打印 [Validation] Request passed
点击查看答案
struct ValidationMiddleware: AsyncMiddleware {
    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        if request.method == .POST {
            print("[Validation] Checking request body...")
            // 实际项目里这里应该 decode body 并验证
            // 但 middleware 里直接 decode 会消耗 body,
            // 所以简单示例只做日志记录
        }
        let response = try await next.respond(to: request)
        if request.method == .POST {
            print("[Validation] Request passed")
        }
        return response
    }
}

// 使用
app.grouped(ValidationMiddleware()).post("api", "users", use: createUserHandler)

注意:在真实项目中,中间件里解码 body 会导致 body 被消费,下游路由无法再读取。Vapor 提供了 Request.BodyStream 的缓冲机制来解决这个问题。实际验证通常放在路由处理函数内部,而不是中间件里。

动手练习 Level 3

目标:构建一个完整的 "Todo" 资源 CRUD API。

用 Vapor 实现以下四个端点:

MethodPath描述请求体响应体
POST/api/todos创建 TodoTodoRequest(title: String)TodoResponse(id, title, done)
GET/api/todos列出所有 Todo[TodoResponse]
GET/api/todos/:id获取单个 TodoTodoResponse
DELETE/api/todos/:id删除 TodoString

提示:用一个 var todos: [TodoResponse] = [] 数组模拟数据库。

点击查看答案
// 1. 定义 Content 模型
struct TodoRequest: Content {
    let title: String
}

struct TodoResponse: Content {
    let id: String
    let title: String
    let done: Bool
}

// 2. 模拟数据库
var todos: [TodoResponse] = []

// 3. 创建 Application
let app = Application(.testing)
defer { app.shutdown() }

// 4. POST /api/todos
app.post("api", "todos") { req -> TodoResponse in
    let reqBody = try req.content.decode(TodoRequest.self)
    let todo = TodoResponse(
        id: UUID().uuidString,
        title: reqBody.title,
        done: false
    )
    todos.append(todo)
    return todo
}

// 5. GET /api/todos
app.get("api", "todos") { req -> [TodoResponse] in
    return todos
}

// 6. GET /api/todos/:id
app.get("api", "todos", ":id") { req -> TodoResponse in
    guard let id = req.parameters.get("id"),
          let todo = todos.first(where: { $0.id == id }) else {
        throw Abort(.notFound, reason: "Todo not found")
    }
    return todo
}

// 7. DELETE /api/todos/:id
app.delete("api", "todos", ":id") { req -> String in
    guard let id = req.parameters.get("id") else {
        throw Abort(.badRequest, reason: "Missing todo ID")
    }
    guard let index = todos.firstIndex(where: { $0.id == id }) else {
        throw Abort(.notFound, reason: "Todo not found")
    }
    todos.remove(at: index)
    return "Deleted todo: \(id)"
}

关键点:

  • Abort(.notFound) 会返回 404 状态码,这是 REST API 的标准做法
  • 返回数组类型 [TodoResponse] 时,Vapor 自动将它编码为 JSON 数组
  • 内存中数组 var todos 在应用重启后会丢失数据,生产环境应该使用数据库

故障排查 FAQ

Q1:Application(.testing) 启动后为什么访问不到服务?

.testing 模式下,Vapor 不会启动实际的 TCP 监听。它只注册路由,让你可以用 app.test(.GET, "api/hello") 做单元测试。如果要真正监听端口,使用 .development.production 并调用 try app.start()

Q2:req.content.decode() 时报 content.source 错误怎么办?

这说明请求没有 Content-Type header 或 body 为空。确保客户端发送请求时设置了 Content-Type: application/json,并且请求体是合法的 JSON。

Q3:中间件里打印的 [Middleware] 日志没有出现?

检查中间件是否正确绑定。如果用 app.grouped()... 注册,中间件只作用于该组内的路由。如果希望全局生效,改用 app.middleware.use(MyMiddleware())

Q4:路径参数 :id 匹配的是 nil

确认路径声明和参数提取使用的是同一个名字。路径写 ":id" 就必须用 req.parameters.get("id")。名字大小写也要一致。另外,Vapor 的路径匹配是按注册顺序进行的,如果前面的路由已经匹配了这个路径,后面的可能就不会执行。

Q5:Abort(.badRequest) 和直接 throw 有什么区别?

Abort 是 Vapor 特有的错误类型,它直接映射到 HTTP 状态码。Abort(.badRequest) 返回 400,Abort(.notFound) 返回 404。如果你 throw 一个普通 Error,Vapor 会默认返回 500 Internal Server Error。

Q6:Vapor 可以跑在 Linux 服务器上吗?

可以。Vapor 完全支持 Linux (Ubuntu 20.04/22.04)。事实上,Vapor 的主要部署场景就是 Linux 服务器 + Docker 容器化部署。只需要保证 Swift 6.0+ 工具链已安装即可。

Q7:多个中间件注册时,执行顺序是怎样的?

中间件按注册顺序正向执行(before 逻辑),按注册顺序反向执行(after 逻辑)。比如注册 A、B 两个中间件,执行流程是:A.before -> B.before -> handler -> B.after -> A.after。这和其他框架(如 Express.js、Koa)的洋葱模型是一致的。

小结

  • Vapor 是 Swift 生态中最成熟的 Web 框架,底层基于 SwiftNIO 的异步 I/O 模型
  • 路由声明用路径片段(Path Segments),参数用 req.parameters.get() 提取,HTTP Method 对应不同方法名
  • Content 协议让请求/响应体的 JSON 编解码和 Codable 无缝对接,不需要额外配置
  • 中间件(AsyncMiddleware)是请求链路中的拦截器,通过 next.respond(to:) 控制请求流转

术语表

中文英文说明
路由RouteHTTP 请求路径和处理函数的映射关系
中间件Middleware请求处理链上的拦截器,在 handler 前后执行自定义逻辑
内容协议Content ProtocolVapor 中请求/响应体的编解码协议,继承自 Codable
路径参数Path ParameterURL 路径中的动态部分,如 /users/:id 中的 id
异步响应器AsyncResponder中间件中代表下一个处理环节的接口类型
应用ApplicationVapor 应用的入口和生命周期管理器
AbortAbortVapor 特有的错误类型,直接映射为 HTTP 状态码

知识检查

用三个问题检验你是否真正掌握了本节内容。

问题一:在 Vapor 中,如果你想让一个结构体能自动解析 JSON 请求体并编码为 JSON 响应体,这个结构体需要遵守什么协议?

查看答案

需要遵守 Content 协议。Content 协议内部继承了 Codable,所以解码/编码逻辑和 JSON 章节中学习的 Codable 完全一致。声明方式:struct UserRequest: Content { ... }

问题二:以下 Vapor 中间件代码中,next.respond(to: request) 这行代码的含义是什么?如果删掉这行会发生什么?

struct LogMiddleware: AsyncMiddleware {
    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        print("Request received")
        let response = try await next.respond(to: request)
        print("Response sent: \(response.status.code)")
        return response
    }
}
查看答案

next.respond(to: request) 的含义是:把请求传递给链路中的下一个处理者(可能是另一个中间件,也可能是最终的路由 handler)。如果删掉这行,请求链就在这里终止了——路由 handler 不会被执行,客户端会收到一个空的或超时的响应。中间件必须要么调用 next.respond(to: request),要么自己返回一个 Response(比如 403 拒绝),否则整个请求就无法完成。

问题三:在 Vapor 中,app.get("api", "users", ":id")app.get("api", "users", "profile") 哪个会先匹配到请求 GET /api/users/profile

查看答案

这取决于路由注册的顺序。Vapor 按照路由注册的顺序进行匹配。如果 :id 先注册,profile 作为字符串字面量匹配在 :id 之后,那么 :id 会先匹配成功,把 profile 作为 id 参数的值。如果 profile 先注册,那么它会优先匹配成功。最佳实践是:始终先注册固定路径(字面量),再注册通配路径(路径参数),否则通配路径会吞噬后续的固定路径。

继续学习

Vapor 帮你处理了 HTTP 层面的请求和响应,数据已经从客户端到达了你的 Swift 代码中。下一步,你需要考虑如何把这些数据持久化存储。

继续学习下一节:GRDB 数据库,你将学会如何用 SQLite 持久化存储 Vapor API 接收到的数据。

如果你想回顾高级进阶的整体路线图,可以返回 高级概览

GRDB SQLite 数据库

开篇故事

想象你经营一家小型游戏竞技场。每天都有玩家来注册、比赛、获得积分。刚开始只有几个人,你拿一张纸就能记住所有人的名字和分数。但一个月后,玩家超过几百人,有人要求查自己的历史成绩,有人想看看排行榜前三名是谁——那张纸已经乱成一团。

GRDB 就像一位专业的赛场记分员。你不需要自己画表格、写 SQL、管理文件指针。你只需要告诉 GRDB:"我有玩家,他们有名字和分数",GRDB 就会帮你建好 SQLite 数据库、创建表结构、处理插入和查询。更棒的是,它用纯 Swift 的协议 (Protocol) 来定义数据模型,写出来的代码和写普通 Swift 结构体一样自然。

你不需要离开 Swift 的世界去学另一门数据库语言。GRDB 让 SQL 数据库操作变成 Swift 类型安全的体验。


本章适合谁

你正在用 Swift 开发需要本地 SQLite 数据库的跨平台应用(macOS、Linux 均可)。你不想手写原始 SQL 语句,也不想引入 CoreData 那样重量级的框架,希望用轻量、类型安全的方式管理关系型数据。

如果你之前用过 FileManager 存 JSON 文件但发现查询效率太低,或者你听说过 SQLite 但不知道如何在 Swift 中优雅地使用它,GRDB 就是为你准备的。


你会学到什么

完成本章后,你可以:

  1. 使用 FetchableRecordPersistableRecord 协议 (Protocol) 定义可持久化的数据模型
  2. 使用 QueryInterface 编写类型安全的查询、过滤和排序
  3. 执行原始 SQL 语句 (Raw SQL) 进行更新和删除操作
  4. 管理 DatabaseQueue 的线程安全访问

前置要求

  • 掌握 Swift 基础语法,尤其是结构体 (struct)、协议 (protocol) 和可选类型 (Optional)
  • 理解 do/try/catch 错误处理模式
  • 理解 CustomStringConvertible 协议用于调试输出
  • macOS 12+ 或 Linux (Ubuntu 20.04+)。GRDB 支持跨平台 SQLite 访问
  • Swift 6.0+ 编译器
  • 已完成本章之前的基础教程章节

提示: GRDB 是一个第三方 SPM (Swift Package Manager) 依赖。在 Package.swift 中添加 .product(name: "GRDB", package: "GRDB.swift") 即可使用。本章示例代码位于 AdvanceSample/Sources/AdvanceSample/GRDBSample.swift


第一个例子

打开代码文件 AdvanceSample/Sources/AdvanceSample/GRDBSample.swift,这是 GRDB 最基础的使用方式:

import GRDB

// 1. 定义数据模型
struct Player: FetchableRecord, PersistableRecord, CustomStringConvertible {
    var id: Int64?
    var name: String
    var score: Int
    
    var description: String {
        "Player(id: \(id ?? -1), name: \(name), score: \(score))"
    }
}

// 2. 创建数据库队列(内存数据库,退出即丢失)
let dbQueue = try DatabaseQueue()

// 3. 在 write 闭包中执行所有数据库操作
try dbQueue.write { db in
    // 创建表
    try db.create(table: "player") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("name", .text).notNull()
        t.column("score", .integer).notNull().defaults(to: 0)
    }
    
    // 插入记录
    var alice = Player(name: "Alice", score: 100)
    try alice.insert(db)
    print("Inserted: \(alice)")
    
    var bob = Player(name: "Bob", score: 200)
    try bob.insert(db)
    print("Inserted: \(bob)")
    
    // 查询所有记录
    let players = try Player.fetchAll(db)
    print("All players: \(players)")
    
    // 条件查询:分数大于 150 的玩家,按分数降序,取第一个
    let topPlayer = try Player
        .filter(Column("score") > 150)
        .order(Column("score").desc)
        .fetchOne(db)
    print("Top scorer: \(topPlayer?.name ?? "none")")
}

运行结果:

--- grdbSample start ---
Inserted: Player(id: 1, name: Alice, score: 100)
Inserted: Player(id: 2, name: Bob, score: 200)
All players: [Player(id: 1, name: Alice, score: 100), Player(id: 2, name: Bob, score: 200)]
Top scorer: Bob
--- grdbSample end ---

整个流程只需要三步:定义模型 → 创建数据库 → 在 write 闭包中操作。GRDB 自动把 Player 结构体的属性映射到 SQLite 表的列。


原理解析

1. DatabaseQueue 与线程安全

DatabaseQueue 是 GRDB 的核心访问入口。它内部维护一个串行队列 (Serial Queue),保证所有数据库操作按顺序执行,不会发生并发写入冲突。

// 内存数据库(测试用,进程退出数据丢失)
let dbQueue = try DatabaseQueue()

// 文件数据库(持久化到磁盘)
let dbURL = URL(fileURLWithPath: "/tmp/game.sqlite")
let fileQueue = try DatabaseQueue(path: dbURL.path)

为什么用 write 闭包? GRDB 的设计理念是"作用域内安全"。dbQueue.write { db in ... } 确保闭包内的所有操作共享同一个数据库连接,并且自动管理事务 (Transaction)。闭包结束时自动提交事务,如果抛出异常则自动回滚。

2. FetchableRecord 与 PersistableRecord 协议

这两个协议是 GRDB 的 ORM (对象关系映射) 核心:

协议作用提供的方法
FetchableRecord从数据库行中解码记录fetchAll(db), fetchOne(db, key:)
PersistableRecord将记录编码并写入数据库insert(db), update(db), delete(db)
struct Player: FetchableRecord, PersistableRecord {
    var id: Int64?          // 可选主键,插入后自动填充
    var name: String        // 映射到 "name" 列
    var score: Int          // 映射到 "score" 列
}

GRDB 默认将属性名直接映射为同名的数据库列。id: Int64? 是可选的,插入后 GRDB 会自动回填自增主键的值。

3. 创建表与 Column 定义

GRDB 提供类型安全的表创建 API:

try db.create(table: "player") { t in
    t.autoIncrementedPrimaryKey("id")    // 自增主键
    t.column("name", .text).notNull()    // TEXT 类型,不允许 NULL
    t.column("score", .integer).notNull().defaults(to: 0)  // 默认值为 0
}

支持的列类型:

Swift 类型SQLite 类型示例
String.textt.column("name", .text)
Int / Int64.integert.column("score", .integer)
Double.doublet.column("price", .double)
Bool.booleant.column("active", .boolean)
Data.blobt.column("image", .blob)
Date.datet.column("created", .date)

列修饰符 (Modifier):

t.column("email", .text).unique()           // 唯一约束
t.column("age", .integer).check { $0 >= 0 } // 检查约束
t.column("role", .text).defaults(to: "user") // 默认值
t.column("bio", .text).notNull()            // 非空约束

4. QueryInterface 查询接口

QueryInterface 是 GRDB 的类型安全查询构建器,让你用 Swift 代码代替原始 SQL:

查询全部:

let players = try Player.fetchAll(db)

按主键查询:

let alice = try Player.fetchOne(db, key: 1)  // id = 1

条件过滤 + 排序:

let topPlayer = try Player
    .filter(Column("score") > 150)           // WHERE score > 150
    .order(Column("score").desc)             // ORDER BY score DESC
    .fetchOne(db)                            // LIMIT 1

QueryInterface 常用操作:

方法对应 SQL说明
.filter(...)WHERE过滤条件
.order(...)ORDER BY排序规则
.limit(n)LIMIT n限制返回数量
.select(...)SELECT选择特定列
.fetchAll(db)执行查询返回数组多条记录
.fetchOne(db)执行查询返回单条零条或一条

5. 原始 SQL 执行

对于复杂的更新和删除操作,GRDB 支持直接执行原始 SQL:

// UPDATE: 使用参数化查询(防 SQL 注入)
try db.execute(
    sql: "UPDATE player SET score = ? WHERE name = ?",
    arguments: [300, "Alice"]
)

// DELETE: 同样使用参数化查询
try db.execute(
    sql: "DELETE FROM player WHERE name = ?",
    arguments: ["Bob"]
)

安全提醒: 永远使用 arguments: 参数绑定值,不要字符串拼接。参数化查询可以防止 SQL 注入攻击。

6. CustomStringConvertible 调试输出

实现 CustomStringConvertible 协议让调试时可以直接打印可读的记录信息:

struct Player: FetchableRecord, PersistableRecord, CustomStringConvertible {
    var id: Int64?
    var name: String
    var score: Int
    
    var description: String {
        "Player(id: \(id ?? -1), name: \(name), score: \(score))"
    }
}

let p = Player(name: "Test", score: 50)
print(p)  // Player(id: -1, name: Test, score: 50)

常见错误

以下错误来源于大量开发者在 GRDB 实际项目中的踩坑总结,按出现频率排列。

错误 1: 表结构不匹配 (Schema Mismatch)

症状: 运行时崩溃 SQLite error 1: no such column: email

// ❌ 表定义时没有 "email" 列,但模型结构体有
struct Player: FetchableRecord, PersistableRecord {
    var id: Int64?
    var name: String
    var email: String  // 表中没有这个列!
    var score: Int
}

修复: 确保模型属性与表列一一对应:

// ✅ 创建表时也要加上 email 列
try db.create(table: "player") { t in
    t.autoIncrementedPrimaryKey("id")
    t.column("name", .text).notNull()
    t.column("email", .text)
    t.column("score", .integer).notNull().defaults(to: 0)
}

错误 2: DatabaseQueue vs DatabasePool 混淆

症状: 高并发场景下出现 database is locked 错误。

// ❌ DatabaseQueue 是串行队列,多个写操作会排队阻塞
let dbQueue = try DatabaseQueue(path: "/tmp/app.sqlite")

// 多线程同时写入 → 后续操作等待前一个完成
Task { try dbQueue.write { db in ... } }
Task { try dbQueue.write { db in ... } }  // 会被阻塞

修复: 如果需要并发读 + 串行写,使用 DatabasePool

// ✅ DatabasePool 支持 WAL 模式,允许多个并发读、一个写
let dbPool = try DatabasePool(path: "/tmp/app.sqlite")

// 并发读取(不会互相阻塞)
Task { try dbPool.read { db in ... } }
Task { try dbPool.read { db in ... } }

// 写入仍然串行化(保证数据一致性)
Task { try dbPool.write { db in ... } }
类型读并发写并发适用场景
DatabaseQueue单线程、测试、简单 CLI
DatabasePool是 (WAL 模式)服务端、高并发读

错误 3: 忘记开启 WAL 模式

症状: DatabasePool 并发读取时仍然出现锁竞争。

GRDB 默认使用传统的 journal 模式。开启 WAL (Write-Ahead Logging) 模式才能发挥 DatabasePool 的并发读优势:

var config = Configuration()
config.readonly = false
// WAL 模式在 DatabasePool 中默认开启,但显式指定更安全
let dbPool = try DatabasePool(path: "/tmp/app.sqlite", configuration: config)

错误 4: 在 write 闭包外使用 db 连接

症状: 编译报错 Cannot use 'db' outside of closure scope 或运行时崩溃。

var dbRef: Database?
try dbQueue.write { db in
    dbRef = db  // ❌ 不要保存 db 引用
}
// dbRef 已失效!

修复: 所有数据库操作必须在 write / read 闭包内完成:

// ✅ 在闭包内完成所有操作
try dbQueue.write { db in
    let players = try Player.fetchAll(db)
    for player in players {
        // 处理数据
    }
}

错误 5: id 类型不匹配

症状: 插入成功但 id 始终为 nil,或者查询时报类型错误。

// ❌ 主键声明为 Int 但表使用 autoIncrementedPrimaryKey (Int64)
struct Player: FetchableRecord, PersistableRecord {
    var id: Int?       // 应该是 Int64?
    var name: String
}

修复: 自增主键始终使用 Int64?

// ✅
struct Player: FetchableRecord, PersistableRecord {
    var id: Int64?
    var name: String
}

Swift vs Rust/Python 对比

维度Swift (GRDB)Rust (Diesel / SQLx)Python (SQLAlchemy)
声明方式结构体 + FetchableRecord / PersistableRecord 协议结构体 + #[derive(Queryable, Insertable)]sqlx::query!declarative_base() 子类
Schema 管理代码内 db.create(table:) 手动定义diesel migration generate 生成 SQL 迁移文件alembic 迁移工具
查询方式QueryInterface 链式 APIDiesel: 类型安全 DSL; SQLx: 编译期 SQL 检查Query API / 方法链
类型安全编译期强检查编译期强检查 (SQLx 宏)运行期检查
线程安全DatabaseQueue 串行 / DatabasePool 并发读连接池 (r2d2 / deadpool)Session 非线程安全
后端存储SQLite(专用)SQLite / PostgreSQL / MySQL几乎所有数据库
原始 SQLdb.execute(sql:arguments:)sqlx::query!()diesel::sql_querysession.execute(text(...))
学习曲线低(纯 Swift API)高(需要理解生命周期、连接池)中(概念丰富但文档好)

核心差异: GRDB 专注于 SQLite 单一后端,API 设计完全遵循 Swift 的协议驱动风格。Rust 的 Diesel 支持多数据库但需要复杂的 schema 定义和迁移工具。Python 的 SQLAlchemy 是最成熟的 ORM 之一,生态最广但也最重。

如果你的应用只需要 SQLite 并且希望用最 Swift 的方式操作数据库,GRDB 是最佳选择。如果你需要 PostgreSQL 或多数据库支持,Rust + SQLx 或 Python + SQLAlchemy 更合适。


动手练习 Level 1

目标:Player 模型增加 email 列,并实现按邮箱查询。

GRDBSample.swift 的基础上:

  1. 给表定义加上 email 列(TEXT 类型)
  2. Player 结构体加上 email: String 属性
  3. 插入一条带邮箱的记录
  4. QueryInterface 按邮箱查询该玩家
查看参考答案
import GRDB

struct Player: FetchableRecord, PersistableRecord, CustomStringConvertible {
    var id: Int64?
    var name: String
    var email: String
    var score: Int
    
    var description: String {
        "Player(id: \(id ?? -1), name: \(name), email: \(email), score: \(score))"
    }
}

let dbQueue = try DatabaseQueue()

try dbQueue.write { db in
    try db.create(table: "player") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("name", .text).notNull()
        t.column("email", .text).notNull()
        t.column("score", .integer).notNull().defaults(to: 0)
    }
    
    var charlie = Player(name: "Charlie", email: "charlie@example.com", score: 150)
    try charlie.insert(db)
    
    // 按邮箱查询
    let found = try Player
        .filter(Column("email") == "charlie@example.com")
        .fetchOne(db)
    
    print("Found by email: \(found ?? nil)")
}

动手练习 Level 2

目标: 实现"排行榜"功能,用 filter + order + limit 查询得分最高的 N 名玩家。

插入 5 名玩家,分数各不相同。查询并打印得分前 3 名的玩家名单(按分数降序排列)。

查看参考答案
try dbQueue.write { db in
    try db.create(table: "player") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("name", .text).notNull()
        t.column("score", .integer).notNull().defaults(to: 0)
    }
    
    let players = [
        Player(name: "Alice", score: 100),
        Player(name: "Bob", score: 300),
        Player(name: "Charlie", score: 200),
        Player(name: "Diana", score: 400),
        Player(name: "Eve", score: 250),
    ]
    for var p in players {
        try p.insert(db)
    }
    
    // 排行榜前 3 名
    let top3 = try Player
        .order(Column("score").desc)
        .limit(3)
        .fetchAll(db)
    
    print("🏆 Top 3 Scorers:")
    for (i, p) in top3.enumerated() {
        print("  \(i + 1). \(p.name) - \(p.score) 分")
    }
}
// 输出:
// 🏆 Top 3 Scorers:
//   1. Diana - 400 分
//   2. Bob - 300 分
//   3. Eve - 250 分

动手练习 Level 3

目标: 构建完整的 Book 模型,实现 CRUD + 事务 (Transaction) 操作。

  1. 定义 Book 结构体,包含 idtitleauthorprice 字段
  2. 创建 book
  3. 实现完整的增、查、改、删操作
  4. 在一个事务中批量插入多本书,如果任何一本插入失败则全部回滚
查看参考答案
import GRDB

struct Book: FetchableRecord, PersistableRecord, CustomStringConvertible {
    var id: Int64?
    var title: String
    var author: String
    var price: Double
    
    var description: String {
        "Book(id: \(id ?? -1), title: \(title), author: \(author), price: \(price))"
    }
}

func bookSample() throws {
    let dbQueue = try DatabaseQueue()
    
    try dbQueue.write { db in
        // 1. 创建表
        try db.create(table: "book") { t in
            t.autoIncrementedPrimaryKey("id")
            t.column("title", .text).notNull()
            t.column("author", .text).notNull()
            t.column("price", .double).notNull().defaults(to: 0.0)
        }
        
        // 2. 创建 (Create) - 批量插入(事务内自动回滚)
        var books = [
            Book(title: "Swift 编程", author: "张三", price: 59.9),
            Book(title: "GRDB 指南", author: "李四", price: 39.9),
            Book(title: "SQLite 实战", author: "王五", price: 49.9),
        ]
        for var book in books {
            try book.insert(db)
        }
        print("插入了 \(books.count) 本书")
        
        // 3. 读取 (Read)
        let allBooks = try Book.fetchAll(db)
        print("书库共有 \(allBooks.count) 本书")
        
        // 4. 更新 (Update) - 修改价格
        if var book = try Book.fetchOne(db, key: 1) {
            book.price = 45.0
            try book.update(db)
            print("更新后: \(book)")
        }
        
        // 5. 删除 (Delete) - 按条件删除
        try db.execute(
            sql: "DELETE FROM book WHERE price < ?",
            arguments: [40.0]
        )
        let remaining = try Book.fetchAll(db)
        print("删除低价书后剩余: \(remaining.count) 本")
    }
}

故障排查 FAQ

Q1: fetchAll 返回空数组,但我明明插入了数据?

首先确认插入操作是否成功。检查 insert(db) 调用后 id 属性是否被自动填充(如果不为 nil 说明插入成功)。其次确认查询和插入是否在同一个 DatabaseQueue 实例上操作。如果是文件数据库,检查路径是否正确,可能你在查一个空文件。

Q2: 运行时报 SQLite error 1: table player already exists

create(table:) 默认在表已存在时报错。如果你希望表不存在才创建(幂等操作),加上 ifNotExists: true 参数:

try db.create(table: "player", ifNotExists: true) { t in ... }

Q3: 如何给已有表添加新列?

GRDB 支持 ALTER TABLE 添加列:

try db.alter(table: "player") { t in
    t.add(column: "email", .text)
}

注意:添加列后需要同步更新 Player 结构体定义,加上对应的属性。

Q4: DatabaseQueue 可以用在多线程中吗?

可以。DatabaseQueue 本身是线程安全的,它内部使用串行队列保证操作顺序。多个线程可以同时调用 dbQueue.write { ... },GRDB 会自动排队执行。但你不应该保存 db 连接引用到闭包外使用。

Q5: 如何查看 GRDB 实际执行的 SQL 语句?

配置 Configuration 开启 SQL 日志:

var config = Configuration()
config.prepareDatabase { db in
    db.trace { event in
        print("SQL: \(event.expandedDescription)")
    }
}
let dbQueue = try DatabaseQueue(configuration: config)

这可以帮助你在调试时确认生成的 SQL 是否符合预期。

Q6: 如何迁移已有数据(Schema 变更)?

GRDB 不提供自动迁移,需要手动编写迁移逻辑:

// 方案 1:先检查表是否存在
let exists = try db.tableExists("player")
if !exists {
    try db.create(table: "player") { t in ... }
} else {
    // 表已存在,检查是否需要添加列
    try db.alter(table: "player") { t in
        t.add(column: "newField", .text)
    }
}

使用 DatabaseMigrator 可以更优雅地管理多版本迁移,详见 GRDB 官方文档的 Migrations 章节。


小结

  • FetchableRecordPersistableRecord 协议将 Swift 结构体与 SQLite 表无缝对接,实现类型安全的 ORM
  • QueryInterface 提供链式 API (filter / order / limit) 替代原始 SQL 查询,编译期即可发现错误
  • DatabaseQueue 保证单线程串行安全,DatabasePool 支持并发读取,根据场景选择合适的数据访问模式
  • db.execute(sql:arguments:) 支持参数化原始 SQL 执行,有效防止 SQL 注入
  • write 闭包自动管理事务,闭包正常结束提交,抛出异常自动回滚

术语表

中文英文说明
记录协议Record ProtocolsFetchableRecord / PersistableRecord,声明结构体可持久化
数据库队列DatabaseQueueGRDB 的核心访问入口,保证串行线程安全
数据库连接池DatabasePool支持并发读取的数据库访问模式,基于 WAL
查询接口QueryInterfaceGRDB 的类型安全链式查询构建器
自增主键Auto-Incremented Primary Key使用 autoIncrementedPrimaryKey 自动生成的唯一 ID
事务Transactionwrite 闭包内的原子操作,成功提交失败回滚
参数化查询Parameterized Query通过 arguments: 绑定值的 SQL 执行,防止注入
写入前日志WAL (Write-Ahead Logging)SQLite 的日志模式,支持并发读取
数据库迁移Database MigrationSchema 变更时手动编写的数据结构升级逻辑

知识检查

题目 1: 下面这段代码有什么问题?如何修复?

struct User: FetchableRecord, PersistableRecord {
    var id: Int?
    var name: String
}

try dbQueue.write { db in
    try db.create(table: "user") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("name", .text).notNull()
    }
    
    var user = User(name: "Alice")
    try user.insert(db)
    print(user.id)  // 这里输出什么?
}
查看答案和解析

问题: id 的类型声明为 Int?,但 autoIncrementedPrimaryKey 返回的是 Int64。在某些 SQLite 实现中主键值可能超过 Int 的范围(32 位系统上限约 21 亿)。虽然在小数据量下可能"能跑",但正确的做法是用 Int64?

修复:

struct User: FetchableRecord, PersistableRecord {
    var id: Int64?    // ✅ 改为 Int64?
    var name: String
}

print(user.id) 在修复后会正确输出 Optional(1)(插入后自动回填)。

题目 2: DatabaseQueueDatabasePool 有什么区别?在高并发的 Web 服务中应该选哪个?

查看答案和解析

DatabaseQueue 使用串行队列,所有操作(读和写)依次执行,不会产生并发,适合简单场景。DatabasePool 基于 SQLite 的 WAL (Write-Ahead Logging) 模式,支持多个并发读操作 + 一个写操作,大幅提升了读密集型场景的性能。

高并发 Web 服务中应该选择 DatabasePool,因为读操作通常远多于写操作。DatabasePool 允许多个读操作并行执行,不会因为一个读操作阻塞另一个读操作。

题目 3: 为什么 GRDB 中更新和删除操作用了原始 SQL (db.execute(sql:arguments:)) 而不是 QueryInterfaceQueryInterface 能实现更新和删除吗?

查看答案和解析

GRDB 的 QueryInterface 主要用于查询(SELECT),更新 (UPDATE) 和删除 (DELETE) 需要使用 RequestupdateAll(db)deleteAll(db) 方法,或者直接执行原始 SQL。

实际上 QueryInterface 也可以实现更新和删除:

// 用 QueryInterface 更新
try Player
    .filter(Column("name") == "Alice")
    .updateAll(db, Column("score").set(to: 300))

// 用 QueryInterface 删除
try Player
    .filter(Column("name") == "Bob")
    .deleteAll(db)

但原始 SQL 在复杂场景下更灵活(如多表 JOIN 更新)。示例代码使用原始 SQL 是为了展示 GRDB 也支持直接执行 SQL 的能力。


继续学习

完成本章后,你已经掌握了用 GRDB 管理 SQLite 数据库的完整能力。下一步可以继续学习高级部分的其他章节:

扩展阅读: GRDB 官方文档提供了更多高级用法,包括数据库观察 (Database Observation)、全文搜索 (FTS5)、关联查询 (Associations) 等特性。访问 GRDB.swift GitHub 仓库 获取完整文档。

🎭 Actors基础 (Actors Basics)

开篇故事

想象一家银行的金库。金库里堆满了现金和珠宝,但只有一位出纳员(Teller)可以同时进出。第二位顾客想取钱?没关系,排队等候。不会出现两个出纳员同时伸手进同一个保险箱,你多取 100、我少记 100 的情况。

Swift 里的 Actor(参与者)就是这个"只能一人进出"的金库。当你有多个任务(Task)同时读写同一份数据时,Actor 保证每次只有一个任务能访问里面的状态。它不是锁(Lock),不是信号量(Semaphore),它是编译器帮你强制执行的安全边界。

在并发编程里,这种"同一时刻只允许一个访问者"的机制叫 互斥(Mutual Exclusion)。Actor 把互斥变成语言级别的原生特性,你不需要手动加锁解锁,编译器替你做了。

本章要教你的,就是如何用 Actor 给你的共享数据穿上一件防弹衣。

本章适合谁

如果你满足以下任一情况,这一章就是为你准备的:

  • 你在写并发代码(Concurrency),多个 Task 会读写同一个可变状态
  • 你曾经遇到过数据竞争(Data Race),两个线程同时改一个变量导致结果错乱
  • 你听说过 Swift Actor 但不太清楚它和 Class 有什么区别
  • 你想用比 NSLock 更 Swift 的方式保证线程安全

本章面向已经理解 async/await 的开发者。如果你还不熟悉异步编程,建议先回顾基础部分的并发编程章节。

你会学到什么

完成本章后,你将掌握以下内容:

  • Actor 定义(Actor Definition):用 actor 关键字声明一个带隔离状态的类型
  • Actor 隔离(Actor Isolation):理解为什么访问 actor 内部的属性和方法必须用 await
  • 跨隔离调用(Crossing Isolation Boundary):掌握 nonisolated 属性的使用场景
  • Actor vs Class:什么时候用 Actor,什么时候用 Class

前置要求

在开始之前,请确保你已掌握以下内容:

  • macOS 12.0+ / Linux(Ubuntu 22.04+)
  • Swift 6.0+ 编译器
  • async / await 异步编程基础(来自 BasicSample 的并发章节)
  • Swift 的类(Class)和结构体(Struct)的基础概念
  • 基本的并发概念:任务(Task)、线程(Thread)

如果你对这些内容还不太熟悉,建议先回顾 ConcurrencySample 中的 async/await 示例,然后再回来。

第一个例子

我们从一个真实的银行场景开始。创建 BankAccount actor,进行存款和取款操作。

这段代码来自 AdvanceSample/Sources/AdvanceSample/ActorSample.swift

// 定义一个 BankAccount actor
actor BankAccount {
    let accountNumber: Int
    var balance: Double
    
    init(accountNumber: Int, initialBalance: Double) {
        self.accountNumber = accountNumber
        self.balance = initialBalance
    }
    
    // 存款:修改隔离状态
    func deposit(amount: Double) {
        balance += amount
    }
    
    // 取款:返回 Bool 表示是否成功(余额不足时返回 false)
    func withdraw(amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
    
    // 查询余额
    func getBalance() -> Double {
        return balance
    }
    
    // nonisolated 属性:不参与隔离,可以直接访问
    nonisolated var description: String {
        "Account #\(accountNumber)"
    }
}

创建实例并调用方法:

func actorSample() async {
    // 创建 actor 实例(注意:actor 的创建不需要 await)
    let account = BankAccount(accountNumber: 1, initialBalance: 1000)
    print("Created: \(account)")
    
    // 调用 actor 方法必须加 await —— 因为你在跨越隔离边界
    await account.deposit(amount: 500)
    let balance = await account.getBalance()
    print("After deposit: \(balance)")  // 输出: 1500.0
    
    // 取款返回 Bool,余额充足时成功
    let success = await account.withdraw(amount: 200)
    print("Withdraw 200: \(success ? "success" : "failed")")  // 输出: success
    
    // 余额不足时取款失败
    let overdrawn = await account.withdraw(amount: 9999)
    print("Withdraw 9999: \(overdrawn ? "success" : "failed")")  // 输出: failed
    
    let finalBalance = await account.getBalance()
    print("Final balance: \(finalBalance)")  // 输出: 1300.0
}

三步流程:创建 actor → await 调用方法 → 获取结果。和调用普通 Class 的区别只在于 await 关键字,但编译器在背后为你做了完全不同的事情。

原理解析

actor 关键字

actor 是 Swift 5.5 引入的核心并发特性。它和 class 一样是引用类型(Reference Type),但内部状态默认处于 隔离域(Isolated Domain)。同一时刻,只有一个执行上下文(Execution Context)能读写 actor 的内部数据。

  ┌─────────────────────────────────┐
  │         BankAccount actor        │
  │  ┌───────────────────────────┐  │
  │  │   isolated state:          │  │
  │  │   accountNumber: Int       │  │
  │  │   balance: Double          │  │
  │  │                            │  │
  │  │   deposit()  ← 串行队列     │  │
  │  │   withdraw() ← 串行队列     │  │
  │  │   getBalance() ← 串行队列   │  │
  │  └───────────────────────────┘  │
  │                                  │
  │  description ← nonisolated 可直接│
  └─────────────────────────────────┘
           ▲         ▲
      Task A     Task B
     (await 调用)  (排队等待)

Actor 隔离(Actor Isolation)

Actor 内部的属性和方法默认为 isolated。当你从 actor 外部(比如一个 Task)访问这些成员时,你在 跨越隔离边界(Crossing Isolation Boundary)。Swift 要求你用 await 标注这个跨越。

// ❌ 编译错误:Actor-isolated instance member cannot be referenced from a non-isolated context
let b = account.getBalance()

// ✅ 正确:用 await 跨越隔离边界
let b = await account.getBalance()

await 不是可选的修饰符。它告诉编译器和运行时:"这个调用可能等待,因为另一个 Task 可能正在操作同一个 actor。"

nonisolated

不是所有属性都需要隔离。nonisolated 标注的成员不参与 actor 的互斥机制,可以在 await 之外直接访问。一般用于:

  • 只读计算属性(不涉及 var 可变状态的读取)
  • 调试用信息(比如 description
  • 常量(let)已经不可变,不需要保护
// description 是 nonisolated 的,因为 accountNumber 是 let,读取时不需要互斥
nonisolated var description: String {
    "Account #\(accountNumber)"
}

// 这行代码不需要 await
print("Created: \(account)")  // 自动调用 description

Actor vs Class

特性ActorClass
类型引用类型引用类型
状态保护编译器强制(Actor Isolation)手动(NSLock / os_unfair_lock)
跨线程访问安全,串行化(Serialized)不安全,需要手动同步
方法调用外部调用必须 await直接调用
重入性(Reentrancy)存在(下文会讲)N/A
声明关键字actorclass

一句话总结:如果一段可变状态会被多个 Task 并发访问,用 Actor 比用 Class + 手动加锁更安全、更简洁。

常见错误

以下是最容易踩到的坑:

错误症状原因修复
遗漏 await编译错误:"Actor-isolated instance member cannot be referenced from a non-isolated context"从 actor 外部调用方法没加 await在所有 actor 方法调用前加 await
可重入性陷阱(Reentrancy)逻辑错误:余额被扣成负数actor 在一个 await 点让出控制权,另一个调用进入导致状态被篡改await 放在尽可能少的地方,或者在操作结束前不跨越 await 点
阻塞等待死锁(Deadlock)在 actor 的方法内部调用另一个 await,而这个 await 又依赖同一个 actor 的响应避免在 actor 方法内调用自己或其他 actor 的 await 方法
隔离域混淆编译错误:"Main actor-isolated property cannot be referenced from a non-isolated context"@MainActor 标注的类型当成普通 actor 使用区分普通 actor 和 @MainActor,后者只能在 Main Thread 上运行

重点讲讲 可重入性(Reentrancy)。Actor 在遇到 await 时会释放控制权,允许其他调用进入:

// 危险:如果 doSomethingElse() 里有 await,另一个 withdraw 可能在它之前执行
actor RiskyBankAccount {
    var balance: Double
    
    func riskyWithdraw(amount: Double) async -> Bool {
        if balance >= amount {
            await someExternalService.verify()  // ← 这里 await 了!
            balance -= amount  // ← 另一个 withdraw 可能已经改过 balance
            return true
        }
        return false
    }
}

正确做法是把所有可变状态操作放在 await 之前完成:

actor SafeBankAccount {
    var balance: Double
    
    func safeWithdraw(amount: Double) async -> Bool {
        guard balance >= amount else { return false }
        balance -= amount  // 同步完成,没有 await
        // 现在才去 await 外部服务
        await someExternalService.logWithdraw(amount)
        return true
    }
}

Swift vs Rust/Python 对比

不同语言的并发安全机制放在一起对比会更有感觉:

特性Swift (Actor)Rust (Mutex)Python (threading.Lock)
定义方式actor BankAccount { ... }let account: Mutex<BankAccount>lock = threading.Lock()
保护机制编译器强制隔离类型系统强制 lock() 才能访问运行时约定,不遵守不会报错
访问语法await account.withdraw(100)account.lock().withdraw(100)with lock: account.withdraw(100)
死锁预防单 actor 串行队列,无嵌套锁编译期检查,Rust 1.63+ 有 Mutex手动管理,容易死锁
可变状态actor 内部的 var锁包装的内部数据with lock: 块内的数据
性能开销消息队列,任务挂起/唤醒系统级锁,内核调度GIL + 线程锁,较慢
学习成本低(关键字 + await)中(需要理解 Ownership)低(库级别 API)

Swift Actor 的独特之处:隔离是编译器强制的,不是约定。你忘了 await 编译器不会让你编译通过。Rust 的 Mutex 也是编译期保证的,但需要理解 Ownership 和 Borrow Checker。Python 的 Lock 完全靠程序员自觉,漏了 with lock: 代码照样跑但可能出 bug。

动手练习 Level 1

目标:创建一个 Counter actor,支持递增和递减。

要求:

  1. 定义 Counter actor,有一个 var count: Int 属性,初始值为 0
  2. 实现 increment() 方法,让 count 加 1
  3. 实现 decrement() 方法,让 count 减 1
  4. 实现 getCount() -> Int 方法,返回当前值
  5. 在一个 Task 中创建 Counter,递增 3 次,递减 1 次,打印最终值
点击查看答案
actor Counter {
    var count: Int = 0
    
    func increment() {
        count += 1
    }
    
    func decrement() {
        count -= 1
    }
    
    func getCount() -> Int {
        count
    }
}

func exercise1() async {
    let counter = Counter()
    await counter.increment()
    await counter.increment()
    await counter.increment()
    await counter.decrement()
    
    let result = await counter.getCount()
    print("最终值: \(result)")  // 输出: 2
}

动手练习 Level 2

目标:创建一个 ThreadSafeArray actor,提供数组的安全读写操作。

要求:

  1. 定义 ThreadSafeArray<T> 泛型 actor,内部持有一个 var items: [T]
  2. 实现 append(_ item: T) 方法,追加元素
  3. 实现 get(_ index: Int) -> T? 方法,按索引获取,索引越界返回 nil
  4. 实现 count() -> Int 方法
  5. [Int] 实例化它,追加 3 个数字,读取索引 1 的元素
点击查看答案
actor ThreadSafeArray<T> {
    private var items: [T] = []
    
    func append(_ item: T) {
        items.append(item)
    }
    
    func get(_ index: Int) -> T? {
        guard items.indices.contains(index) else { return nil }
        return items[index]
    }
    
    func count() -> Int {
        items.count
    }
}

func exercise2() async {
    let safeArray = ThreadSafeArray<Int>()
    await safeArray.append(10)
    await safeArray.append(20)
    await safeArray.append(30)
    
    let item = await safeArray.get(1)
    let count = await safeArray.count()
    
    print("索引 1 的值: \(item ?? -1)")  // 输出: 20
    print("数组长度: \(count)")          // 输出: 3
}

泛型 Actor 和泛型 Struct/Class 的声明方式完全一样,把 <T> 放在 actor 关键字后面即可。

动手练习 Level 3

目标:构建一个 Bank 来管理多个账户,支持账户间转账。

要求:

  1. 复用上面的 BankAccount actor
  2. 定义 Bank 类,内部维护 [Int: BankAccount] 字典(用账户号映射)
  3. 实现 createAccount(accountNumber: Int, initialBalance: Double) 方法
  4. 实现 transfer(from: Int, to: Int, amount: Double) async -> Bool 方法:从源账户取钱,存到目标账户
  5. 测试:创建两个账户,转账后检查余额变化

注意:转账涉及两个不同的 actor 交替操作await 取钱之后,可能在存钱之前另一个操作干扰了源账户。这里演示了多 actor 协调的复杂性。

点击查看答案
actor BankAccount {
    let accountNumber: Int
    var balance: Double
    
    init(accountNumber: Int, initialBalance: Double) {
        self.accountNumber = accountNumber
        self.balance = initialBalance
    }
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func withdraw(amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
    
    func getBalance() -> Double {
        balance
    }
    
    nonisolated var description: String {
        "Account #\(accountNumber)"
    }
}

class Bank {
    private var accounts: [Int: BankAccount] = [:]
    
    func createAccount(accountNumber: Int, initialBalance: Double) {
        accounts[accountNumber] = BankAccount(
            accountNumber: accountNumber,
            initialBalance: initialBalance
        )
    }
    
    func transfer(from: Int, to: Int, amount: Double) async -> Bool {
        guard let fromAccount = accounts[from],
              let toAccount = accounts[to] else {
            print("账户不存在")
            return false
        }
        
        // 先从源账户取款
        let withdrawn = await fromAccount.withdraw(amount: amount)
        guard withdrawn else {
            print("余额不足,转账失败")
            return false
        }
        
        // 再存入目标账户
        await toAccount.deposit(amount: amount)
        return true
    }
    
    func getBalance(accountNumber: Int) async -> Double? {
        guard let account = accounts[accountNumber] else { return nil }
        return await account.getBalance()
    }
}

func exercise3() async {
    let bank = Bank()
    bank.createAccount(accountNumber: 1, initialBalance: 1000)
    bank.createAccount(accountNumber: 2, initialBalance: 500)
    
    print("转账前:")
    if let b1 = await bank.getBalance(accountNumber: 1) {
        print("账户 1: \(b1)")
    }
    if let b2 = await bank.getBalance(accountNumber: 2) {
        print("账户 2: \(b2)")
    }
    
    let ok = await bank.transfer(from: 1, to: 2, amount: 300)
    print("转账 300: \(ok ? "成功" : "失败")")
    
    print("转账后:")
    if let b1 = await bank.getBalance(accountNumber: 1) {
        print("账户 1: \(b1)")  // 输出: 700.0
    }
    if let b2 = await bank.getBalance(accountNumber: 2) {
        print("账户 2: \(b2)")  // 输出: 800.0
    }
}

注意这里的 transfer 方法本身 没有 标记为 actor,它是一个普通方法。它内部通过 await 分别调用两个 actor 的方法来协调操作。真正的并发安全由每个 BankAccount actor 自身保证。

故障排查 FAQ

Q1:为什么调用 actor 方法必须加 await,创建 actor 却不需要?

创建 actor 只是分配内存空间(和创建 class 一样),此时还没有任何 Task 在访问它的内部状态。await 是跨越隔离边界时需要的,创建动作本身不涉及隔离。

Q2:nonisolated 可以标注方法吗?

可以。nonisolated func someMethod() { ... } 表示这个方法不从 actor 外部调用也需要 await。但要注意:nonisolated 方法不能访问 actor 内的 var 属性,只能访问 let 常量和其他 nonisolated 成员。

Q3:Actor 里能不能用 DispatchQueue 或者其他同步原语?

技术上可以,但不推荐。Actor 的设计目的就是取代手动同步原语。如果你在 actor 里又用了 NSLockDispatchSemaphore,说明设计可能有问题。把状态保护交给 actor,不要在它里面再加一层锁。

Q4:多个 actor 同时调用会不会死锁?

单个 actor 不会产生死锁,因为它的消息队列是串行的(FIFO)。但多个 actor 之间互相 await 可能产生死锁,比如 Actor A 等 Actor B,Actor B 又在等 Actor A。避免方法:不要做循环依赖的跨 actor 调用。

Q5:Actor 和 @MainActor 有什么区别?

@MainActor 是一个标注了特定执行器的 actor —— 它绑定到主线程(Main Thread),常用于 UI 更新。普通 actor 不绑定到任何特定线程,Swift 运行时会自动调度。如果你在 UIKit/AppKit 环境下从后台 Task 更新 UI,编译器会报错,这时用 @MainActor 标注你的方法。

Q6:actor 内部调用自己的方法要不要加 await

不需要。在 actor 内部调用自己的方法属于 同隔离域调用(Same-Isolation Call),编译器知道不会有并发冲突。

actor BankAccount {
    var balance: Double = 0
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func depositTwice(amount: Double) {
        deposit(amount: amount)  // 不需要 await
        deposit(amount: amount)  // 不需要 await
    }
}

小结

  • Actor 是 Swift 的并发安全类型,用 actor 关键字声明,内部状态默认隔离
  • 访问隔离成员必须用 await,这是编译器强制的跨越隔离边界操作
  • nonisolated 让你标记不需要保护的成员(常量、只读计算属性),避免不必要的异步开销

术语表

中文英文说明
参与者ActorSwift 5.5 引入的并发安全引用类型,保证内部状态互斥访问
隔离域Isolated DomainActor 内部状态的受保护区域,外部访问需跨越隔离边界
隔离边界Isolation Boundary从 actor 外部访问其隔离成员的边界线,跨越时须用 await
非隔离Nonisolated标注不参与 actor 隔离的成员,可直接访问不需要 await
可重入性ReentrancyActor 在 await 点释放控制权的现象,可能导致状态被其他调用篡改
串行化Serialization多个 Task 对同一 actor 的访问按顺序执行,而非并行
执行上下文Execution Context执行 actor 方法的运行时环境,同一时刻只有一个

知识检查

用三个问题检验你是否真正掌握了本节内容。

问题一:以下代码为什么编译失败?

actor Counter {
    var value: Int = 0
}

func main() {
    let c = Counter()
    print(c.value)  // ← 编译错误
}
查看答案

value 是 actor 隔离的实例成员,从 main() 这个非隔离上下文中访问需要 await。正确写法:

func main() async {
    let c = Counter()
    print(await c.value)  // 加 await 跨越隔离边界
}

问题二nonisolated 标注的方法能不能修改 actor 内部的 var 属性?

查看答案

不能。nonisolated 表示该方法不参与 actor 的互斥机制,因此编译器不允许它访问或修改可变状态(var)。只能读取 let 常量和访问其他 nonisolated 成员。如果需要修改 var,方法必须是隔离的(不加 nonisolated)。

问题三:Actor 和加了 NSLock 的 Class 在功能上有什么区别?

查看答案

功能上等价:都保证了同一时刻只有一个线程读写共享状态。但区别在于:

  1. 编译器强制 vs 开发者约定:Actor 漏了 await 编译不过;Class + Lock 漏了 lock() 编译通过但运行会出错
  2. 语法简洁度:Actor 天然集成 async/await;Class + Lock 需要手动包装 lock() / unlock()
  3. 死锁风险:单 Actor 无嵌套锁不会死锁;Class + Lock 嵌套使用可能死锁
  4. 重入性:Actor 有可重入特性(await 时让出控制权);Lock 是阻塞式等待

继续学习

Actor 是 Swift 并发模型的基石之一。理解了基础的 actor 隔离和 await 调用,你可以进一步深入了解:

  • Sendable 深入:数据在 Task 和 Actor 之间传递时需要遵守 Sendable 协议,编译器会确保数据跨线程安全

如果还想回顾整个高级进阶的内容目录,查看 高级概览

Sendable 深入理解

📦 开篇故事

想象你在做跨境电商。你要把包裹从中国寄到美国,途中要经过海关、航空货运、快递分拣中心,每个环节都有不同的人在搬运你的包裹。

什么样的包裹能安全到达?——密封完好的标准化箱子。里面装的是衣服、书本这类不会自己变的东西。即使两个包裹同时上路,各自的内容也不会互相影响。

什么样的包裹会出问题?——一个开着盖的活体动物箱。里面的兔子会在途中跑出来,可能跑到另一个箱子里去。如果两个人同时打开箱子检查,兔子状态就不可控了。

Swift 中的 Sendable 协议就是海关的检查标准:只有"密封好、内容安全"的数据才能被送到不同的并发任务中去。 值类型(struct、enum)天然密封,引用类型(class)如果有可变状态就是"开着盖的箱子"。Swift 6 的严格并发检查(Strict Concurrency Checking)就是海关,它会拦住所有不安全的包裹。


👤 本章适合谁

你正在使用 Swift 6.0 的严格并发检查模式-strict-concurrency=complete),编译器频繁报 Sendable 相关的警告或错误。你想理解:

  • 为什么你的 struct 自动就是 Sendable,而 class 不是
  • @Sendable 闭包标注到底在约束什么
  • 如何系统性地修复 non-Sendable capture 错误

如果你刚接触 Swift 并发,建议先阅读基础部分的 并发编程 章节,理解 async/awaitTaskActor 的基本用法后再回来。


🎯 你会学到什么

完成本章后,你可以:

  • 理解 Sendable 协议的本质:什么样的类型可以安全地跨并发边界传递
  • 掌握 @Sendable 闭包标注:如何声明一个闭包可以在不同线程安全执行
  • 系统性修复 Sendability 错误:识别非 Sendable 捕获、值类型隐式发送、@unchecked Sendable 等场景

📋 前置要求

  • macOS 12.0+ 或 Linux(Ubuntu 22.04+)
  • Swift 6.0+:本章基于 Swift 6 的严格并发检查
  • 理解 async/await 异步编程模型
  • 理解 Task 的创建和并发执行
  • 建议已阅读基础部分 并发编程 章节,了解 Actor 的基本概念

⚠️ 平台提醒: Sendable 是 Swift 5.5 引入、Swift 6 强化的标准库协议,所有平台通用。但严格的编译期检查只在 Swift 6 的 complete 模式下才会全面生效。


🚀 第一个例子

打开代码文件 AdvanceSample/Sources/AdvanceSample/ConcurrencyDeepSample.swift,这是一个最简单的 Sendable 使用场景:

struct UserData: Sendable {
    let name: String
    let age: Int
}

UserData 是一个值类型(struct),所有属性都是不可变(let)且本身也是 Sendable 的类型(StringInt)。所以它天然可以安全地在并发任务之间传递。

接下来创建两个并发任务,同时读取同一份 UserData

func sendableSample() async {
    let data = UserData(name: "Alice", age: 30)

    let task1 = Task {
        print("Task 1 sees: \(data.name), age \(data.age)")
        return data.age
    }

    let task2 = Task {
        try await Task.sleep(for: .milliseconds(10))
        print("Task 2 also sees: \(data.name)")
        return data.name
    }

    let age = await task1.value
    let name = await task2.value
    print("Results: \(name) is \(age)")
}

运行结果:

--- sendableSample start ---
Task 1 sees: Alice, age 30
Task 2 also sees: Alice
Results: Alice is 30
--- sendableSample end ---

两个 Task 并发执行,各自读取 data 的内容。因为 UserDataSendable,编译器知道这是安全的——两个任务拿到的是各自独立的数据副本(值语义),不存在数据竞争。


🔬 原理解析

1. Sendable 协议是什么

Sendable 是 Swift 标准库中的一个标记协议(marker protocol):

public protocol Sendable {
}

它没有任何方法要求,作用纯粹是告诉编译器:"这个类型的实例可以安全地从并发执行域(actor、Task、线程)传递到另一个。"

当一个类型遵循 Sendable 时,编译器会验证它确实满足安全条件。如果不满足,编译报错。

2. 隐式 Sendable(Implicit Sendability)

不是所有类型都需要显式写 : Sendable。以下类型自动被视为 Sendable,编译器自动帮你加上了这个标签:

类型自动 Sendable?条件
struct✅ 是所有存储属性都是 Sendable
enum(无关联值)✅ 是所有关联值类型都是 Sendable
tuple✅ 是所有元素类型都是 Sendable
let 不可变属性✅ 是属性本身不可变且类型 Sendable
final class❌ 否必须有 @unchecked Sendable 或手动声明(即使所有属性是 let)
class(非 final)❌ 否子类可修改父类状态,永远不自动 Sendable

这就是为什么前面的 UserData struct 即使不写 : Sendable 编译器也知道它是 Sendable,但加上显式标注可以让意图更明确。

3. @Sendable 闭包标注

闭包会捕获它所在作用域里的变量。当闭包被送到另一个并发任务中执行时,如果捕获的变量不是 Sendable,就会发生数据竞争。

@Sendable 标注强制编译器检查闭包捕获的所有外部变量是否满足 Sendable:

let closure: @Sendable () -> String = {
    return "Sendable closure captured: \(data.name)"
}
let result = Task(operation: closure)
print(await result.value)
// 输出: Sendable closure captured: Alice

Task(operation:) 本身就要求传入一个 @Sendable 闭包。如果 data 不是 Sendable 类型,这段代码编译就会报错。

4. 编译器的强制检查

在 Swift 6 严格模式下,以下场景编译器会报错:

  • 非 Sendable 类型在 Task 闭包内被捕获non-Sendable type 'XXX' captured by sendable closure
  • 非 Sendable 类型作为 Sendable 函数的参数传递
  • 非 Sendable 类型从 Actor 隔离域传出

编译器不会放过任何一个可能的数据竞争隐患。

5. @unchecked Sendable( unchecked 发送)

有些类型你确信是线程安全的(比如内部用了锁),但编译器无法自动推断。这时可以用 @unchecked Sendable 手动告诉编译器"我保证安全":

class ThreadSafeCounter: @unchecked Sendable {
    private var _count: Int = 0
    private let lock = NSLock()

    var count: Int {
        lock.lock()
        defer { lock.unlock() }
        return _count
    }

    func increment() {
        lock.lock()
        defer { lock.unlock() }
        _count += 1
    }
}

⚠️ 警告@unchecked Sendable 等于关闭了编译器的 Sendable 检查。如果你保证错误,运行时不会有任何提示,直接导致数据竞争和未定义行为。只在确实理解并发安全的情况下使用。

6. 非 Sendable 的典型场景

回到源代码中的对比:

class NonSendableCounter {
    var count: Int = 0
}

这是一个普通的 class,有可变属性 var count。如果两个 Task 同时修改它,就会发生数据竞争:

let counter = NonSendableCounter()

let t1 = Task { counter.count += 1 }   // ⚠️ 编译警告
let t2 = Task { counter.count += 1 }   // ⚠️ 编译警告

await t1.value
await t2.value
print(counter.count)                   // 可能是 1 或 0 或 2 — 不可预测

Swift 6 严格模式会直接拦下这段代码。修复方式:改用 actor 保护状态,或者确保不跨 Task 访问。


❌ 常见错误

以下错误来源于大量开发者在实际项目中踩坑后的总结,按出现频率排列。

错误 1: 非 Sendable 类型在 @Sendable 闭包中被捕获

症状: 编译报错 Capture of 'xxx' with non-Sendable type 'yyy' in a @Sendable closure

class Config {
    var apiKey: String = ""
}

func runTask() async {
    let config = Config()  // Config 是 class,不是 Sendable

    Task {
        // ⚠️ 编译错误:config 是非 Sendable 的,闭包不能捕获它
        print("Using key: \(config.apiKey)")
    }
}

修复: 用 Sendable 的值类型替代 class,或在 Task 外部提取需要的值:

// 方案 A:改为 struct(值类型自动 Sendable)
struct Config {
    let apiKey: String
}

// 方案 B:在闭包外部提取值
func runTask() async {
    let config = Config()
    let key = config.apiKey  // String 是 Sendable

    Task {
        print("Using key: \(key)")  // ✅ 只捕获了 key(String),安全
    }
}

错误 2: Sendability 违反:非 Sendable 参数传入 Sendable 函数

症状: 编译报错 Sending 'xxx' risks causing data races

func processUserData(_ data: some Sendable) {
    // 要求参数必须 Sendable
}

class MutableUser {
    var name: String = ""
}

let user = MutableUser()
processUserData(user)  // ⚠️ 编译错误:MutableUser 不是 Sendable

修复: 将 class 改为 struct,或确保参数类型遵循 Sendable:

struct ImmutableUser: Sendable {
    let name: String
}

let user = ImmutableUser(name: "Alice")
processUserData(user)  // ✅ ImmutableUser 是 Sendable

错误 3: Actor 数据竞争:从外部直接访问 Actor 内部的可变状态

症状: 编译报错 Actor-isolated property 'xxx' cannot be referenced from non-isolated context

actor DataStore {
    var items: [String] = []

    func add(_ item: String) {
        items.append(item)
    }
}

let store = DataStore()
Task {
    store.items.append("hello")  // ⚠️ 编译错误:不能直接写 Actor 内部状态
}

修复: 通过 Actor 的方法间接访问:

Task {
    await store.add("hello")  // ✅ 通过 Actor 方法,自动 await 隔离
}

⚔️ Swift vs Rust/Python 对比

维度Swift (Sendable)Rust (Send trait)Python
并发安全机制Sendable 协议 + 编译器检查Send trait + 借用检查器(Borrow Checker)无内置机制,靠开发者自觉(GIL 部分保护)
值类型可发送struct/enum 自动推断几乎所有类型默认 Send(不含 Rc/RefCell无值类型/引用类型区分
引用类型可发送class 需显式标注 @unchecked Sendable需手动实现 Sendunsafe impl一切皆引用,多线程共享但无保护
闭包发送@Sendable 标注 + 捕获变量检查move 闭包 + Send boundlambda 可跨线程,但需手动同步
检查时机编译期(Swift 6 严格模式)编译期(强制,无例外)运行期(threading 模块自行处理)
数据竞争保证编译拦截大部分场景编译期零数据竞争保证无保证(GIL 仅保护 CPython 字节码执行)

核心差异: Rust 的 Send 是所有并发安全的基石,编译器 100% 保证数据竞争不会发生。Swift 的 Sendable 是一个渐进式改进——Swift 5.5/5.10 下可以警告但不报错,Swift 6 严格模式才全面启用。Python 完全没有并发安全机制,需要手动使用锁。

如果你在 Swift 中习惯了 Sendable 的安全感,切换到 Python 多线程时需要格外小心。Rust 开发者会觉得 Swift 的 Sendable 很熟悉,但 Rust 的保证更彻底。


🏋️ 动手练习 Level 1

目标: 将一个 struct 变成真正的 Sendable 类型

下面这段代码定义了一个 UserProfile struct,但包含了一个非 Sendable 的属性。修复它使整个 struct 成为 Sendable:

// ❌ 问题:closure 不是 Sendable 类型
struct UserProfile {
    let name: String
    let age: Int
    var onLoad: () -> Void  // 函数类型不是 Sendable
}

提示: Sendable 类型的所有存储属性必须都是 Sendable。想想哪些属性不是 Sendable 的,如何修改?

查看参考答案
// ✅ 方案:移除非 Sendable 的属性
struct UserProfile: Sendable {
    let name: String
    let age: Int
    // onLoad 函数类型不是 Sendable,不能作为存储属性
}

// 如果确实需要回调行为,可以用 Actor 或 enum 封装:
actor UserProfileHandler {
    private let profile: UserProfile

    init(profile: UserProfile) {
        self.profile = profile
    }

    func onProfileLoaded() {
        print("\(profile.name) loaded")
    }
}

核心原则:struct 中不能有函数类型的存储属性(包括闭包),因为它们不是 Sendable。


🏋️ 动手练习 Level 2

目标: 修复 @Sendable 闭包捕获错误

以下代码尝试在 Task 中捕获一个 class 实例,但 class 不是 Sendable:

class AppConfig {
    var serverURL: String = "https://api.example.com"
}

func fetchConfig() async {
    let config = AppConfig()

    Task {
        // ⚠️ 编译错误:config 是非 Sendable 的 class
        let url = config.serverURL
        print("Fetching from: \(url)")
    }
}

修复这段代码,让 Task 能安全地访问 serverURL 的值。

查看参考答案
// 方案 A:在 Task 外部提取值(最简单)
func fetchConfig() async {
    let config = AppConfig()
    let url = config.serverURL  // String 是 Sendable,可以安全捕获

    Task {
        print("Fetching from: \(url)")  // ✅ 只捕获 String
    }
}

// 方案 B:将 class 改为 struct(如果不需要引用语义)
struct AppConfig {
    let serverURL: String
}

func fetchConfig() async {
    let config = AppConfig(serverURL: "https://api.example.com")

    Task {
        print("Fetching from: \(config.serverURL)")  // ✅ struct 是 Sendable
    }
}

关键区别:方案 A 只提取了需要的值(String),方案 B 把整个类型变成了 Sendable。


🏋️ 动手练习 Level 3

目标: 用 Sendable + Actor 构建一个线程安全的配置管理器

实现一个 ThreadSafeConfigManager

  1. actor 包裹可变配置状态
  2. 配置数据本身用 Sendable struct 表示
  3. 支持 get()set() 方法
  4. 支持多个 Task 并发读写
查看参考答案
// Sendable 配置数据(值类型)
struct ConfigEntry: Sendable {
    let key: String
    let value: String
}

// Actor 保护状态
actor ThreadSafeConfigManager {
    private var store: [String: String] = [:]

    func set(_ entry: ConfigEntry) {
        store[entry.key] = entry.value
    }

    func get(key: String) -> String? {
        return store[key]
    }

    func getAll() -> [ConfigEntry] {
        return store.map { ConfigEntry(key: $0.key, value: $1.value) }
    }
}

// 使用示例
func runConfigSample() async {
    let manager = ThreadSafeConfigManager()

    // 并发写入和读取
    async let setDb = manager.set(ConfigEntry(key: "database", value: "sqlite"))
    async let setCache = manager.set(ConfigEntry(key: "cache", value: "redis"))

    await setDb
    await setCache

    let db = await manager.get(key: "database")
    let all = await manager.getAll()

    print("Database config: \(db ?? "not found")")
    print("All entries: \(all.count)")
}

这里 ConfigEntry struct 是 Sendable 的,可以自由进出 Actor 边界。Actor 内部的可变 store 字典被隔离保护,外部只能通过 await 方法安全访问。


🔧 故障排查 FAQ

Q1: 我的 struct 明明没有可变属性,为什么编译器还说它不是 Sendable?

检查 struct 中是否包含非 Sendable 类型的属性。常见的非 Sendable 类型包括:闭包 () -> Void、class 实例、UnsafePointerDispatchQueue。即使属性是 let 不可变,只要类型本身不是 Sendable,整个 struct 就不是 Sendable。

Q2: @Sendable 和不加 @Sendable 的 Task 闭包有什么区别?

Task 初始化器本身要求一个 @Sendable 闭包,所以即使你不显式写 @Sendable,闭包也会被编译器当作 Sendable 闭包来处理。显式标注主要用于自定义函数接受闭包参数的场景:

func runAsync(_ operation: @Sendable () async -> Int) async {
    let result = await operation()
    print(result)
}

Q3: Swift 5.x 和 Swift 6 下的 Sendable 行为有什么不同?

Swift 5.10 及之前:Sendable 检查默认是警告(warning),可以忽略,编译仍然通过。Swift 6 默认开启严格并发检查:Sendable 违反变成编译错误(error),必须修复。如果你的项目在 Swift 6 下从警告突然变错误,这就是原因。

Q4: 我可以让 class 自动成为 Sendable 吗?

final class 且所有存储属性都是 let 不可变 + Sendable 类型时,你可以显式标注 : Sendable,编译器会验证。但 class 永远不会"自动"成为 Sendable(不像 struct/enum)。这是因为 class 本质是可变的引用类型,子类可以添加可变状态。如果需要引用语义且 Sendable,建议改用 actor

Q5: @unchecked Sendable 到底什么时候用?

当你确信一个类型是线程安全的,但编译器无法推断时。典型场景:

  • 内部使用了 NSLock/os_unfair_lock 等同步原语
  • 使用了 C 库的线程安全 API
  • 第三方库类型的适配

绝大多数情况不需要 @unchecked Sendable。优先用 struct、Actor 来解决。@unchecked 是最后的手段。

Q6: 为什么 Task.sleep 需要 try await?

Task.sleep(for:)async throws 函数,可能因 Task 被取消而抛出 CancellationError。在实际代码中通常用 try await 处理,或者用 try? await 忽略取消错误:

try? await Task.sleep(for: .milliseconds(10))  // 取消时不报错

📌 小结

  • Sendable 是 Swift 的并发安全标记协议:值类型(struct、enum)在所有属性 Sendable 时自动遵循,class 需要显式处理
  • @Sendable 闭包标注确保闭包捕获的所有外部变量都是 Sendable 的,防止跨线程数据竞争
  • 数据竞争问题的根本解决模式:值类型优先 → Actor 包裹可变状态 → @unchecked Sendable 作为最后手段
  • Swift 6 的严格并发检查将所有 Sendable 违规从警告升级为错误,强制开发者在编译期解决并发安全问题

📖 术语表

术语英文说明
可发送协议Sendable标记类型可以安全跨并发边界传递的协议
隐式可发送Implicit Sendabilitystruct/enum/tuple 自动被编译器视为 Sendable 的行为
闭包捕获Closure Capture闭包引用外部作用域变量的机制
严格并发检查Strict Concurrency CheckingSwift 6 编译选项,将并发安全违规升级为编译错误
未检查发送@unchecked Sendable手动声明 Sendable 但跳过编译器验证的方式
数据竞争Data Race多个并发任务同时读写同一可变状态导致的未定义行为
Actor 隔离Actor IsolationActor 确保其内部状态只能被串行访问的机制
值语义Value Semantics每次传递都复制数据副本,不存在共享可变状态

✅ 知识检查

题目 1: 下面哪个类型是 Sendable 的?为什么?

// A
struct Point {
    let x: Int
    let y: Int
}

// B
class Counter {
    var value: Int = 0
}

// C
enum Status {
    case idle
    case running
    case completed
}
查看答案和解析

A 和 C 是 Sendable,B 不是。

  • A(Point struct):所有属性是 let 且类型为 Int(Sendable),自动 Sendable
  • B(Counter class):class 有可变 var 属性,不是 Sendable。引用语义 + 可变状态 = 数据竞争风险
  • C(Status enum):无关联值的 enum,自动生成 Sendable

题目 2: 下面代码会编译通过吗?如果不通过,会报什么错误?

func processUser(user: some Sendable) {
    print("Processing user")
}

class User {
    var name: String = ""
}

let u = User()
processUser(user: u)
查看答案和解析

不会编译通过。

错误信息类似:Sending 'u' risks causing data races; 'User' is not Sendable

User class 有可变属性 var name,不遵循 SendableprocessUser 要求参数必须是 some Sendable,类型不匹配。修复方式:将 User 改为 struct 或确保只传不可变数据。

题目 3: 以下 @unchecked Sendable 使用是否正确?如果不正确,有什么问题?

class SharedState: @unchecked Sendable {
    var items: [String] = []

    func add(_ item: String) {
        items.append(item)
    }

    func getAll() -> [String] {
        return items
    }
}

let state = SharedState()

Task {
    state.add("item1")
}
Task {
    state.add("item2")
}
查看答案和解析

编译能通过,但运行时有数据竞争问题。

@unchecked Sendable 告诉编译器"我保证这个类型线程安全",但 SharedStateitems 数组没有任何同步保护(锁、Actor 等)。两个 Task 同时调用 add() 会导致并发写入 [String],这是经典的数据竞争。

@unchecked Sendable 只是让编译器闭嘴,不自动提供线程安全。正确的做法是把 SharedState 改为 actor,或在内部使用锁保护。


➡️ 继续学习

完成本章后,你已经掌握了 Swift 并发安全的核心概念。下一步:

  • 属性包装器:学习 @Published@AppStorage、自定义 @propertyWrapper,理解属性如何在 SwiftUI 和并发场景中发挥更大作用
  • 高级进阶总览:回顾整个高级部分的学习路径,规划下一步探索方向

扩展阅读: 想了解 Swift 并发模型的完整设计哲学?参考 Apple 官方文档 Meet Swift Concurrency WWDC 2021 演讲,以及 Swift Evolution 提案 SE-0302(Sendable 协议的原始提案)。

属性包装器 (Property Wrappers)

开篇故事

想象你收到一份生日礼物,拆开外面的包装盒后,里面是一个普通的马克杯。但别急,包装盒上有个自动感温贴纸,只要环境温度超过 35 度,它就会变红提醒你。盒子底部还装了一个小型除湿器,能保证杯子里的茶不会受潮发霉。

你看,马克杯本身没有任何变化,它还是那个马克杯。但包装盒给它加上了温度监控和防潮功能,让普通的杯子变得智能起来。

属性包装器(Property Wrapper)就是编程世界里的这种礼物盒。它不改变属性本身的数据类型和存储方式,但在属性读写的时候"额外做一些事情":限制范围、自动修剪、记录日志、校验格式等等。你只需要在属性前面加一个 @ 前缀,就能复用这些行为。

本章要教你的,就是如何打造自己的"智能包装盒",让属性自带能力。

本章适合谁

如果你满足以下任一情况,这一章就是为你准备的:

  • 你发现自己反复在 didSet 里写相同的校验逻辑,想要 DRY(Don't Repeat Yourself)
  • 你用过 SwiftUI 的 @State@Binding,想知道背后的原理
  • 你想创建可复用的属性模式,在一个地方定义,全局使用
  • 你对泛型(Generics)有基本了解,想看看泛型在实际工程中的应用

本章面向已经会写基础 Swift 语法和泛型基本概念的开发者。

你会学到什么

完成本章后,你将掌握以下内容:

  • @propertyWrapper 属性:如何声明一个属性包装器结构体
  • wrappedValue:包装值,属性的实际存储和访问入口
  • projectedValue:投影值,用 $ 前缀访问的额外通道
  • 泛型包装器:让 @Clamped<Value: Comparable> 支持任意可比较类型
  • 包装器组合:在同一个属性上叠加多个包装器
  • 常见陷阱:初始化匹配、投影类型混淆、包装顺序问题

前置要求

在开始之前,请确保你已掌握以下内容:

  • Swift 基础语法:变量声明、结构体(Struct)、属性(Property)
  • 属性观察器(Property Observers):didSetwillSet 的用法
  • 泛型基础(Generics):类型参数 <T> 和类型约束 where T: Comparable
  • 闭包范围(ClosedRange):0...100 这样的区间语法

如果你对这些内容还不太熟悉,建议先回顾基础部分(结构体与类 → 泛型 → 错误处理),然后再回来。

⚠️ 环境要求: macOS 12+ 或 Linux,Swift 6.0+。属性包装器是 Swift 5.1 引入的特性,在 Swift 6.0 中行为稳定。

第一个例子

我们先来看一个最直观的例子:用 @Clamped 包装器确保玩家的分数永远在 0-100 之间,不管你赋什么值。

代码文件位于 AdvanceSample/Sources/AdvanceSample/PropertyWrapperSample.swift 第 1-14 行:

@propertyWrapper
struct Clamped<Value: Comparable> {
    var wrappedValue: Value {
        didSet {
            wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound)
        }
    }
    var projectedValue: Clamped { self }
    let range: ClosedRange<Value>
    
    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }
}

使用方式:

struct Player {
    @Clamped(0...100) var score: Int = 50
}

var player = Player()

player.score = 150
print(player.score)  // 输出: 100

player.score = -10
print(player.score)  // 输出: 0

不管你给 score 赋什么值,它都会被自动限制在 0 到 100 的范围内。赋值 150 变成 100,赋值 -10 变成 0。包装器在幕后默默完成了裁剪逻辑,你调用时完全感觉不到它的存在——这正是好包装器的标志。

原理解析

属性包装器的核心机制可以拆解成以下几个关键概念。

@propertyWrapper 属性

在结构体前面加上 @propertyWrapper,编译器就会把它识别为属性包装器。这个结构体必须包含一个名为 wrappedValue 的属性,它是包装器与被包装属性之间的桥梁。

@propertyWrapper
struct Logged<Value> {
    var wrappedValue: Value {
        didSet {
            print("  [Logged] Changed to: \(wrappedValue)")
        }
    }
    
    init(wrappedValue: Value) {
        self.wrappedValue = wrappedValue
        print("  [Logged] Initialized: \(wrappedValue)")
    }
}

Logged 包装器每次属性值变化时都会打印日志。使用方式:

struct Player {
    @Logged var name: String = "Unknown"
}

var player = Player()
// 输出: [Logged] Initialized: Unknown

player.name = "Alice"
// 输出: [Logged] Changed to: Alice

player.name = "Bob"
// 输出: [Logged] Changed to: Bob

wrappedValue 存储

wrappedValue 是包装器的核心属性。当你这样写:

@Clamped(0...100) var score: Int = 50

编译器实际上会生成类似这样的代码:

private var _score = Clamped(wrappedValue: 50, 0...100)
var score: Int {
    get { _score.wrappedValue }
    set { _score.wrappedValue = newValue }
}

访问 player.score 时,你实际上访问的是 _score.wrappedValue。赋值时,didSet 观察者会触发裁剪逻辑。

projectedValue 和 $ 语法

projectedValue 是包装器提供的第二个通道,用 $ 前缀访问。它让你在不绕过包装器的前提下拿到包装器本身的元信息。

struct Player {
    @Clamped(0...100) var score: Int = 50
}

let player = Player()
print(player.$score.projectedValue.range)  // 输出: ClosedRange(0...100)

$score 访问的是 projectedValue,在这里例子中,projectedValue 返回 Clamped 自身(self),所以你可以继续访问 .range 获取限制范围的上下界。

为什么需要 projectedValue? 假设你想在运行时动态读取属性的约束条件(比如 UI 上显示一个分数条的上下限),projectedValue 就是为此而生的。

泛型包装器

Clamped<Value: Comparable> 用泛型让同一个包装器支持任意可比较的类型。IntDoubleFloat,甚至自定义的比较类型,只要遵守 Comparable 协议就能用。

@Clamped(0.0...1.0) var opacity: Double = 0.5
@Clamped(1...10) var volume: Int = 5

一个包装器定义,多处类型复用。泛型是属性包装器能保持通用性的关键。

包装器组合

你可以在同一个属性上叠加多个包装器,但要注意顺序。Swift 按照从上到下的顺序依次应用:

struct UserProfile {
    @Logged
    @Clamped(0...100)
    var level: Int = 1
}

外层是 Logged,内层是 Clamped。每次修改 level 时,先经过 Clamped 裁剪,再经过 Logged 记录。组合顺序不同,行为也不同。

常见错误

以下是最容易踩到的坑。

错误症状解决方案
初始化参数不匹配编译错误:Extra argument in callinit(wrappedValue: ..., ...) 中第一个参数必须是 wrappedValue,这是 Swift 的语法约定。如果你定义的包装器 init 不叫这个名字,编译器无法将 @Wrapper(arg) var x: T = default 翻译成正确的调用
projectedValue 类型混淆运行时拿到预期外的类型projectedValue 的返回类型由你定义,不一定等于 wrappedValue 的类型。明确声明返回类型,不要依赖推断
包装器顺序导致行为异常外层包装器的逻辑没有生效多个包装器组合时,最上面的是最外层。@A @B var x 等价于 x = A(wrappedValue: B(...)),A 包裹 B
泛型约束缺失编译错误:cannot find 'min' in scopeComparable 约束不能少。没有 Comparableminmax 无法使用
值类型 vs 引用类型混淆包装器修改了但外部看不到包装器本身是 struct(值类型)还是 class(引用类型)会影响 projectedValue 的行为。大多数场景下用 struct 即可

Swift vs Python/Rust 对比

不同语言对于"给属性附加行为"有不同的设计思路,放在一起对比会更有感觉:

特性Swift (Property Wrapper)Python (Descriptor)Rust
声明方式@propertyWrapper struct实现 __get__/__set__ 方法的类无直接等价物
使用方式@Clamped var x: Intx = Clamped() 在类内声明手动在 getter/setter 中实现逻辑
投影值$x projectedValue无内建投影概念
类型安全编译期检查运行期检查编译期检查(手动实现)
泛型支持原生泛型 <Value: Comparable>泛型通过 typing.Generic原生泛型 <T>
组合方式多个 @ 叠加多个 Descriptor 叠加困难通过嵌套 impl 块或宏

Swift 的属性包装器是三者中最优雅的。Python 的 Descriptor 历史悠久但语法冗长,Rust 没有内建等价物,开发者通常在闭包或宏中手动实现类似逻辑。

动手练习 Level 1

目标:创建一个 @NonEmpty 属性包装器,确保 String 类型的属性永远不会变成空字符串。

要求:

  1. 定义泛型约束为 String 类型的包装器
  2. 如果尝试赋空字符串 "",自动恢复为上一次的值或默认值
  3. didSet 中打印警告信息
点击查看答案
@propertyWrapper
struct NonEmpty {
    private var _value: String
    var wrappedValue: String {
        get { _value }
        set {
            if newValue.isEmpty {
                print("  [NonEmpty] 警告: 不能设置为空字符串,保持原值")
            } else {
                _value = newValue
            }
        }
    }
    
    init(wrappedValue: String) {
        if wrappedValue.isEmpty {
            fatalError("NonEmpty: 初始值不能为空")
        }
        _value = wrappedValue
    }
}

struct UserProfile {
    @NonEmpty var username: String = "guest"
}

var user = UserProfile()
user.username = "alice"    // 正常赋值
user.username = ""         // 输出: 警告: 不能设置为空字符串,保持原值
print(user.username)       // 输出: alice

动手练习 Level 2

目标:创建一个 @Trimmed 属性包装器,自动去除字符串首尾的空白字符。

要求:

  1. 赋值时自动调用 .trimmingCharacters(in: .whitespacesAndNewlines)
  2. 初始化时也自动裁剪
  3. 支持泛型,约束为 String
点击查看答案
@propertyWrapper
struct Trimmed {
    var wrappedValue: String {
        didSet {
            wrappedValue = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
        }
    }
    
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

struct Article {
    @Trimmed var title: String
}

let article = Article(title: "  Swift 属性包装器教程  ")
print("《\(article.title)》")
// 输出: 《Swift 属性包装器教程》

动手练习 Level 3

目标:创建一个 @ValidatedEmail 属性包装器,使用正则表达式校验邮箱格式。

要求:

  1. 使用 Swift 5.7+ 的 Regex 类型匹配邮箱格式
  2. 如果格式不合法,抛出 ValidationError 错误
  3. projectedValue 返回验证结果状态(isValid: Bool)
点击查看答案
@propertyWrapper
struct ValidatedEmail {
    struct ValidationError: Error, CustomStringConvertible {
        let message: String
        var description: String { message }
    }
    
    private static let emailRegex = /^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
    
    private var _value: String
    var isValid: Bool { Self.emailRegex.wholeMatch(in: _value) != nil }
    
    var wrappedValue: String {
        get { _value }
        set {
            guard Self.emailRegex.wholeMatch(in: newValue) != nil else {
                print("  [ValidatedEmail] 非法邮箱格式: \(newValue)")
                return
            }
            _value = newValue
        }
    }
    
    var projectedValue: Bool { isValid }
    
    init(wrappedValue: String) {
        _value = wrappedValue
    }
}

struct Account {
    @ValidatedEmail var email: String = ""
}

var account = Account()
account.email = "alice@example.com"     // 合法,赋值成功
account.email = "not-an-email"          // 非法,被拒绝
print("邮箱合法: \(account.$email)")     // 输出: 邮箱合法: true

故障排查 FAQ

Q1:@propertyWrapper 只能用在 struct 上吗?

不是。属性包装器可以声明在 structclassenum 上。但绝大多数场景下用 struct 就足够了。包装器本身应该是轻量的值类型。

Q2:wrappedValueprojectedValue 的类型可以不同吗?

可以。这是非常常见的做法。比如 @ClampedwrappedValueValue(比如 Int),而 projectedValueClamped<Value> 结构体自身或者一个自定义的状态类型。$score 拿到的不一定是 Int

Q3:属性包装器可以用在函数参数上吗?

可以,但有语法限制。Swift 5.7+ 支持在函数参数前加包装器,但参数必须用 _ 做外部标签:

func setScore(@Clamped(0...100) _ score: Int) {
    print(score)  // 自动被裁剪
}

Q4:为什么我的包装器 didSet 没有在初始化时触发?

Swift 的行为约定:didSet赋值 时触发,但不在初始化时触发。如果需要在初始化时也执行验证或转换逻辑,直接在 init(wrappedValue:) 里写。

Q5:多个 @propertyWrapper 叠加时,如何判断谁先谁后?

从上到下,最上面的是最外层。@A @B var x 意味着 _x 的类型是 A<B<T>>。A 的 wrappedValue 是 B 包装器,B 的 wrappedValue 才是最终的实际值。理解这层嵌套关系后,行为就容易预测了。

Q6:属性包装器能访问所属的类型(Type)吗?

不行。属性包装器本身不持有对包含它的结构体或类的引用。如果你需要跨属性通信(比如属性 A 的变化触发属性 B 的更新),需要用其他方式,比如 ObservableObject + @Published

Q7:@Clamped 里用了 didSet,初始化时赋值会被裁剪吗?

会的。因为 init(wrappedValue:) 里手动执行了一次 min(max(...)) 裁剪。didSet 只在后续赋值时触发。好的包装器应该在初始化和赋值两个路径上都执行相同的逻辑。

小结

  • 属性包装器(Property Wrapper)是给属性加行为的"智能包装盒",不改变数据类型本身
  • wrappedValue 是属性的实际值,projectedValue$ 前缀访问,提供额外的元信息通道
  • 泛型约束(<Value: Comparable>)让包装器通用化,一个定义支持多种类型
  • 多个包装器可以叠加,但要注意顺序:最上面的是最外层

术语表

英文中文说明
Property Wrapper属性包装器通过 @propertyWrapper 声明的结构体,为属性附加额外行为
wrappedValue包装值包装器的核心属性,直接映射到被包装属性的读写操作
projectedValue投影值包装器提供的额外通道,通过 $ 前缀访问,返回自定义类型
Comparable可比较协议Swift 标准库协议,支持 <><=>= 比较操作
ClosedRange闭区间Swift 中的闭区间类型,写作 0...100,包含上下界
didSet属性观察者属性值变化后触发的代码块,是包装器实现自动验证的关键
Generic Type Constraint泛型类型约束对泛型参数的限制,如 Value: Comparable,确保类型具备特定能力

知识检查

用三个问题检验你是否真正掌握了本节内容。

问题一:当你写 @Clamped(0...100) var score: Int = 50 时,Swift 编译器实际生成的代码结构是什么?

查看答案

编译器会生成一个私有包装器实例 _score,以及一个计算属性 score

private var _score = Clamped(wrappedValue: 50, 0...100)
var score: Int {
    get { _score.wrappedValue }
    set { _score.wrappedValue = newValue }
}

你访问 score 时,实际上在访问 _score.wrappedValue。赋值时,didSet 观察者执行裁剪逻辑。

问题二player.$scoreplayer.score 有什么区别?它们的类型分别是什么?

查看答案
  • player.score 类型是 Int,返回的是 wrappedValue,也就是属性的实际值
  • player.$score 类型是 Clamped<Int>,返回的是 projectedValue。在代码中,projectedValue 返回 self,所以你可以通过 $score.range 访问 ClosedRange<Int> 类型的限制范围

简单记:没有 $ 是值,有 $ 是包装器本体。

问题三:如果我要写一个 @Debounced 包装器,让属性变化 1 秒后才真正触发更新,这个包装器需要包含什么关键元素?

查看答案
@propertyWrapper
struct Debounced<Value> {
    var wrappedValue: Value {
        didSet {
            Task {
                try await Task.sleep(for: .seconds(1))
                // 1 秒后才执行实际逻辑
            }
        }
    }
    let onChange: (Value) -> Void
    
    init(wrappedValue: Value, onChange: @escaping (Value) -> Void) {
        self.wrappedValue = wrappedValue
        self.onChange = onChange
    }
}

关键元素:wrappedValuedidSet 使用 Task.sleep 做延迟,一个回调闭包 onChange 在延迟后执行实际逻辑。这展示了属性包装器如何和 Swift 并发编程 (Concurrency) 结合。

继续学习

属性包装器让你用声明式语法控制属性的行为。Swift 进阶中还有很多类似的声明式工具,它们都用 @ 前缀,背后的设计思想是相通的。

继续学习下一节:异步编程与 SwiftNIO,你将了解如何用 Taskasync/await 在异步环境中工作。

自动引用计数 (ARC) 与内存管理

开篇故事

想象你借了一本图书馆的书。管理员在借书记录上画一道竖线:今天只有你一个人借了这本书,计数为 1。过了一天,你的同事也来借了同一本,管理员又画一道竖线——计数变成 2。还书的时候,管理员每看到一个人归还,就划掉一道竖线。当竖线数量归零时,管理员知道"没有任何人在使用了",这本书就可以放回书架或者销毁。

这段图书馆的故事,本质上就是 Swift 的自动引用计数 (Automatic Reference Counting, ARC) 的工作原理。每一个类 (class) 的实例在内存中都有一个"引用计数器"。每当你创建一个强引用 (strong reference),计数器加一;每次引用消失,计数器减一。当计数器归零时,系统自动释放这块内存,调用 deinit

但 ARC 并非永远聪明。想象这样一本书:你在书的借阅卡上写着自己的名字,书上也在借阅卡上写着你的名字——两个记录互相指向对方,谁都不先划掉,书就永远无法归还。这就是循环引用 (retain cycle),也是本章最核心的问题。学会用 weakunowned 打破循环,是每个 Swift 开发者的必修课。

示例代码位于 AdvanceSample/Sources/AdvanceSample/ARCSample.swift


本章适合谁

如果你写过以下任何一种代码,理解 ARC 对你来说不是可选知识,而是必学技能:

  • 使用过 class 类型并在对象间建立了互相引用的关系
  • 写过闭包 (closure),尤其是捕获了 self 的闭包
  • 用过代理模式 (delegate pattern)、观察者模式 (observer pattern)、或任何回调机制
  • 遇到过内存泄漏 (memory leak) 或程序莫名崩溃
  • 想要深入理解 Swift 的内存模型,写出更高效、更安全的生产代码

本章适合已经掌握 Swift 基础语法、了解 classstruct 区别的开发者。


你会学到什么

完成本章后,你可以:

  1. 理解 ARC 的引用计数机制以及它何时释放对象
  2. 区分强引用 (strong)、弱引用 (weak)、无主引用 (unowned) 的使用场景
  3. 识别循环引用 (retain cycle) 并用 weakunowned 打破它
  4. 掌握闭包中的捕获列表 (capture list):[weak self][unowned self][value]
  5. 使用 deinit 调试对象生命周期,验证实例是否被正确释放

前置要求

  • 掌握 Swift 基础语法,特别是 class 类型、可选类型 (optional) 和 init 构造器
  • 理解引用类型 (reference type) 和值类型 (value type) 的区别
  • 了解闭包 (closure) 的基本语法
  • 已阅读高级进阶概览章节

第一个例子

打开 AdvanceSample/Sources/AdvanceSample/ARCSample.swift,先看最核心的 Person/Pet 示例:

class Person {
    let name: String
    var pet: Pet?

    init(name: String) {
        self.name = name
        print("  Person \(name) initialized")
    }

    deinit {
        print("  Person \(name) deinitialized")
    }
}

class Pet {
    let name: String
    weak var owner: Person?

    init(name: String) {
        self.name = name
        print("  Pet \(name) initialized")
    }

    deinit {
        print("  Pet \(name) deinitialized")
    }
}

关键点Petowner 属性被标记为 weak。这正是打破循环引用的关键。

接下来看这些对象如何被创建和释放:

func arcSample() {
    print("--- arcSample start ---")

    var person: Person? = Person(name: "Alice")
    var pet: Pet? = Pet(name: "Fluffy")

    person?.pet = pet
    pet?.owner = person
    print("Cycle created (pet.owner is weak, so no retain cycle)")

    person = nil
    print("After person = nil")

    pet = nil
    print("After pet = nil")

    // ... closure example below
}

运行输出

--- arcSample start ---
  Person Alice initialized
  Pet Fluffy initialized
Cycle created (pet.owner is weak, so no retain cycle)
  Person Alice deinitialized
After person = nil
  Pet Fluffy deinitialized
After pet = nil

发生了什么?

  1. Person Alice 被创建,引用计数 = 1(person 变量持有它)
  2. Pet Fluffy 被创建,引用计数 = 1(pet 变量持有它)
  3. person?.pet = petpersonpet 属性强引用了 FluffyFluffy 的计数 = 2
  4. pet?.owner = personpetowner弱引用Alice 的计数 不增加(仍然 = 1)
  5. person = nilperson 变量放弃持有,Alice 的计数 = 0 → 触发 deinit
  6. Alice 释放后,它对 Fluffy 的强引用也消失,Fluffy 的计数 = 1
  7. pet = nilpet 变量放弃持有,Fluffy 的计数 = 0 → 触发 deinit

如果 owner 不是 weak 而是默认的强引用,步骤 4 中 Alice 的计数会变成 2。步骤 5 后 Alice 的计数 = 1(pet.owner 还强引用着),不会释放。同样 pet 也不会释放——两个对象就永远留在了内存中,这就是内存泄漏


原理解析

1. ARC 工作机制

Swift 不使用垃圾回收 (garbage collection),而是使用自动引用计数。编译器在编译时自动在合适的位置插入引用计数操作:

  • 当一个引用开始持有某个对象时,编译器插入 retain(计数加一)
  • 当一个引用不再持有某个对象时,编译器插入 release(计数减一)
  • 当计数归零时,实例被销毁,deinit 被自动调用

ARC 是自动的:你不需要手动调用 retain 或 release。但它只作用于 class——structenum 是值类型,不走引用计数。

2. 强引用、弱引用、无主引用

引用类型关键字是否增加计数返回类型何时使用
强引用默认(无修饰符)Type对象拥有关系(如 Person.pet
弱引用weakType?(可选)父→子中的子→父反向引用
无主引用unownedType(非可选)子对象的生命周期一定不短于父对象

weak 的核心特性

  • 必须声明为可选类型 (Type?)
  • 当引用对象被释放时,自动设为 nil——不会变成悬空指针
  • 因此访问 weak 变量永远是安全的

unowned 的核心特性

  • 声明为非可选类型 (Type)
  • 当引用对象被释放时,不会自动设为 nil
  • 如果访问已经被释放的 unowned 引用,程序会崩溃(runtime crash)
  • 适合"子对象的生存期一定不短于父对象"的场景,例如信用卡 CreditCard 和客户 Customer 的关系——信用卡不可能在没有客户的情况下独立存在

3. 闭包中的捕获列表 (Capture List)

闭包会捕获它访问的所有外部变量。对于引用类型,闭包默认使用强引用捕获,这同样会导致循环引用:

class DataProcessor {
    var data: [Int] = []

    lazy var processData: () -> Void = {
        self.data.append(42)  // 闭包强引用 self → 循环引用!
    }
}

解决方案:在闭包参数前使用捕获列表。有三种写法:

// 写法 1: [weak self] — 最常用,安全
lazy var processData: () -> Void = { [weak self] in
    self?.data.append(42)  // self 是可选的,需要 ? 解包
}

// 写法 2: [unowned self] — 适合 self 一定存活时
lazy var processData: () -> Void = { [unowned self] in
    self.data.append(42)  // self 不是可选的,直接用
}

// 写法 3: [value] — 值类型的捕获(值拷贝)
var localValue = 42
let closure = { [localValue] in
    print("  Closure captured: \(localValue)")
}
closure()  // 输出: Closure captured: 42

值类型捕获的意义:如果不使用捕获列表 {},闭包会"随用随读"变量的当前值;使用 [localValue] 会把变量在闭包定义时的值拷贝进去,之后的修改不会影响闭包内读取到的值。这在异步回调和定时器等场景中非常有价值。


常见错误

错误原因后果解决方案
循环引用 (Retain Cycle)两个 class 互相强引用内存泄漏,deinit 不执行一方改用 weakunowned
闭包捕获 self 导致循环lazy var 闭包强引用 selfself 持有闭包内存泄漏使用 [weak self] 捕获列表
unowned 引用在对象释放后被访问对象已经 deinit,但仍有代码访问 unowned 引用运行时崩溃 (EXC_BAD_ACCESS)改用 weak,或确保对象生命周期更长
结构体中使用 weak值类型不走引用计数,weak 仅适用于 class编译错误改用 class 或移除 weak
delegate 未用 weak最常见的 retain cycle 来源内存泄漏delegate 协议必须声明为 weak var delegate

Swift vs Rust/Python 对比

特性Swift (ARC)Rust (Ownership/Borrowing)Python (GC)
内存管理方式编译时插入 retain/release所有权系统 + 借用检查器运行时标记-清除垃圾回收
运行时开销较小的引用计数操作无运行时开销GC 暂停 (stop-the-world)
循环引用可能发生,需手动用 weak/unowned 打破编译期直接拒绝(借用检查器)可能发生,依赖 GC 的循环检测
引用类型class 走 ARC,struct 走值类型单一所有权,Rc<T> 可共享所有对象都是引用
指针安全weak 自动 nil 化,unowned 需手动保证借用检查器在编译期保证安全依赖 GC,无编译期保证
内存释放时机确定性的(计数归零立即释放)确定性的(离开作用域立即释放)不确定的(GC 何时运行不可控)

Swift 的独特优势:ARC 比 Python 的 GC 更快更确定(没有 GC 暂停),又比 Rust 更容易上手(不需要学习所有权的复杂规则)。但你需要保持警惕:编译器不会像 Rust 的借用检查器那样阻止你写出循环引用——这是开发者自己的责任。


动手练习 Level 1

创建一个 TreeNode 类,要求:

  • 每个节点有 name: String
  • children: [TreeNode](强引用)
  • parent: TreeNode?弱引用
  • 实现 deinit,打印节点名称来验证释放
点击查看答案
class TreeNode {
    let name: String
    var children: [TreeNode] = []
    weak var parent: TreeNode?

    init(name: String) {
        self.name = name
        print("  TreeNode '\(name)' initialized")
    }

    deinit {
        print("  TreeNode '\(name)' deinitialized")
    }

    func addChild(_ child: TreeNode) {
        children.append(child)
        child.parent = self
    }
}

// 测试
func testTreeNode() {
    let root = TreeNode(name: "Root")
    let child1 = TreeNode(name: "Child1")
    let child2 = TreeNode(name: "Child2")
    root.addChild(child1)
    root.addChild(child2)
    // root 释放时,children 也会释放,parent 是 weak,不会阻止释放
}

动手练习 Level 2

以下代码中存在循环引用。找出问题并用 weak 修复它:

protocol WeatherServiceDelegate: AnyObject {
    func didReceiveTemperature(_ temp: Double)
}

class WeatherService {
    var delegate: WeatherServiceDelegate?  // ← 问题在这里

    func fetchTemperature() {
        // 模拟从网络获取温度
        delegate?.didReceiveTemperature(25.0)
    }

    deinit {
        print("  WeatherService deinitialized")
    }
}

class ViewController {
    let service: WeatherService

    init() {
        service = WeatherService()
        service.delegate = self  // 循环引用!
    }

    deinit {
        print("  ViewController deinitialized")
    }
}

extension ViewController: WeatherServiceDelegate {
    func didReceiveTemperature(_ temp: Double) {
        print("  当前温度: \(temp)°C")
    }
}
点击查看答案

问题:ViewController 强引用 WeatherServiceWeatherServicedelegate 又强引用 ViewController,形成循环。

修复:把 delegate 改为 weak。注意协议必须声明为 AnyObject(只有 class 类型才能实现协议并支持 weak 属性)。

// 协议声明为 AnyObject 约束
protocol WeatherServiceDelegate: AnyObject {
    func didReceiveTemperature(_ temp: Double)
}

class WeatherService {
    weak var delegate: WeatherServiceDelegate?  // ← 关键修复

    func fetchTemperature() {
        delegate?.didReceiveTemperature(25.0)
    }

    deinit {
        print("  WeatherService deinitialized")
    }
}

// 其余代码不变

动手练习 Level 3

实现一个轻量级的观察者模式 (Observer Pattern),要求:

  • Notifier 类有一个 addObserver(_:) 方法,接收 Observable 协议类型的对象
  • Observable 协议必须声明为 AnyObject,内部有一个 notify(data:) 方法
  • 观察者应该使用弱引用存储,确保被观察的对象释放后,Notifier 不会阻止它被释放
  • 实现 removeObserver(_:) 方法
  • 实现 notifyAll(data:) 方法通知所有观察者
点击查看答案
protocol Observable: AnyObject {
    func notify(data: String)
}

class Notifier {
    // 使用 NSHashTable 自动处理 weak 引用(最优雅的方案)
    private var observers = NSHashTable<AnyObject>.weakObjects()

    func addObserver(_ observer: any Observable) {
        observers.add(observer)
    }

    func removeObserver(_ observer: any Observable) {
        observers.remove(observer)
    }

    func notifyAll(data: String) {
        for case let observer as Observable in observers.allObjects {
            observer.notify(data: data)
        }
    }
}

// 如果不想用 NSHashTable,也可以用数组手动清理:
// class NotifierManual {
//     private var observers: [WeakObservable] = []
//
//     func addObserver(_ observer: any Observable) {
//         observers.append(WeakObservable(object: observer))
//     }
//
//     func notifyAll(data: String) {
//         // 清理已释放的观察者
//         observers = observers.filter { $0.object != nil }
//         for wrapper in observers {
//             wrapper.object?.notify(data: data)
//         }
//     }
// }
//
// class WeakObservable {
//     weak var object: (any Observable)?
//     init(object: any Observable) { self.object = object }
// }

// 使用示例
class Logger: Observable {
    let name: String
    init(name: String) { self.name = name }
    func notify(data: String) {
        print("  [\(name)] 收到通知: \(data)")
    }
    deinit { print("  Logger '\(name)' deinitialized") }
}

func testObserver() {
    let notifier = Notifier()
    var logger1: Logger? = Logger(name: "Debug")
    var logger2: Logger? = Logger(name: "Info")

    notifier.addObserver(logger1!)
    notifier.addObserver(logger2!)

    notifier.notifyAll(data: "Hello Observers")

    logger1 = nil  // Logger 'Debug' deinitialized
    notifier.notifyAll(data: "Only Info logger now")  // 只剩 Info

    logger2 = nil  // Logger 'Info' deinitialized
}

故障排查 FAQ

Q1. 如何确认我的代码是否存在循环引用?

deinit 中加入 print 语句。如果预计某个对象在某个时间点应该被释放,但你没有看到 deinit 的打印输出,那大概率存在循环引用。另一种方式是使用 Xcode 的 Instrument 工具 → "Leaks" → "Allocations" 追踪内存变化。

Q2. weakunowned 应该用哪个?

简单判断标准:

  • 如果引用的对象可能在访问前就被释放了 → 用 weak(安全,返回可选值)
  • 如果引用的对象一定在访问前不会被释放 → 用 unowned(更快,非可选)

在实际开发中,weak 更常用,因为它更安全。unowned 只在你有绝对把握时使用,一旦出错就是运行时崩溃。

Q3. struct 可以使用 weak 吗?

不行。weak 只适用于引用类型(class)。struct 是值类型,不涉及引用计数,自然也不存在 weak 的概念。如果你在 struct 中声明 weak var,编译器会报错。

Q4. 为什么闭包捕获列表 [weak self]self 变成了可选类型?

weak 引用的本质就是可选类型。当被引用的对象被释放时,weak 引用会自动设为 nil。因此闭包内的 self 类型从 SomeClass 变成了 SomeClass?,你需要用 self?.someMethod()guard let self = self else { return } 来安全使用。

Q5. unowned 引用被访问后程序崩溃了,怎么办?

崩溃信息通常是 EXC_BAD_ACCESS 或类似 "attempted to read an unowned reference but the object was already deallocated"。这说明你的假设错了——被 unowned 引用的对象已经被释放。最直接的修复方式是改为 weak,并在访问前做 nil 检查。如果你确认对象不应该被释放,那说明别的地方有逻辑错误导致对象被提前释放了。

Q6. 闭包什么时候需要用 [weak self],什么时候不需要?

判断标准是:闭包是否被持有并且和 self 之间可能形成循环。如果闭包是临时使用的(如 array.filter { ... }),闭包执行完就会销毁,不需要 [weak self]。但如果闭包被存储为一个属性(如 lazy var)或者被异步任务持有,那就必须检查是否形成了强引用循环。

Q7. Swift 的 ARC 和 Objective-C 的 ARC 有什么区别?

本质上是一样的——都是基于引用计数的内存管理。区别在于 Swift 的 ARC 更严格:

  • Swift 中 protocol 要实现 weak 必须声明 : AnyObject 约束
  • Swift 引入了 unowned(unsafe)(与 Objective-C 的 __unsafe_unretained 对应)
  • Swift 5.7+ 引入了 borrowingconsuming 关键字用于更细粒度的控制

小结

  • ARC 是 Swift 的内存管理机制,通过引用计数器决定何时释放 class 实例——计数归零即释放
  • weak 引用不增加计数且会在对象释放后自动变为 nil,是打破循环引用的首选方式
  • 闭包默认强引用外部变量,使用捕获列表 [weak self][unowned self][value] 来控制捕获行为

术语表

术语说明
ARC (自动引用计数, Automatic Reference Counting)Swift 的内存管理方式,编译器自动在合适位置插入 retain/release 操作,计数归零时释放对象
强引用 (Strong Reference)默认引用方式,增加引用计数。声明时不加任何修饰符即为强引用
弱引用 (Weak Reference)weak 修饰的引用,不增加计数,对象释放后自动设为 nil,必须用于可选类型
无主引用 (Unowned Reference)unowned 修饰的引用,不增加计数,对象释放后不会自动设为 nil,访问已释放对象会崩溃
捕获列表 (Capture List)闭包参数前的方括号语法 [weak self], [unowned self], [value],用于控制闭包如何捕获外部变量
循环引用 (Retain Cycle)两个或多个 class 实例互相强引用导致各自的引用计数永远大于零,内存无法释放
deinitclass 的析构函数,在实例引用计数归零、被释放前自动调用,用于清理资源和调试验证

知识检查

问题 1: 以下代码的输出是什么?

class A {
    var b: B?
    deinit { print("A deinit") }
}

class B {
    weak var a: A?
    deinit { print("B deinit") }
}

var a: A? = A()
var b: B? = B()
a?.b = b
b?.a = a
a = nil
b = nil
查看答案
A deinit
B deinit

b?.a 是弱引用,不增加 A 的计数。a = nilA 的计数归零 → 先触发 A deinitA 释放后对 B 的强引用也消失了,B 的计数变为 1(只剩变量 b 持有)。b = nilB 的计数归零 → 触发 B deinit

问题 2: 如果把问题 1 中 Bweak var a: A? 改为 var a: A?(强引用),输出是什么?

查看答案

没有任何输出AB 互相强引用,形成了循环引用。a = nil 只是移除了外部变量对 A 的引用,但 B.a 还在强引用 A。同理 b = nil 也无法释放 B,因为 A.b 还在强引用 B。两个对象都不会被释放,deinit 不会被调用——这就是内存泄漏。

问题 3: 以下闭包中 [localValue] 的作用是什么?如果去掉 [localValue],输出会有什么变化?

var localValue = 42
let closure = { [localValue] in
    print(localValue)
}
localValue = 100
closure()
查看答案

输出:42

[localValue]localValue 在闭包定义时的值(42)值拷贝到闭包中。之后 localValue = 100 的修改不会影响闭包内部的值。

如果去掉 [localValue] 改成普通闭包 { print(localValue) },闭包会读取 localValue当前值(因为 localValue 是值类型,闭包会捕获它的引用),输出变为 100


继续学习

完成了 ARC 与内存管理的学习,你已经掌握了 Swift 中管理对象生命周期的核心技能。接下来可以:

Opaque/Existential 类型 (Opaque/Existential Types)

开篇故事

想象你去餐厅点菜。服务员告诉你"今日特色菜"——你知道这是一道菜,但不知道具体是什么。这就是 Opaque Type(不透明类型):你只知道它符合某个标准(Protocol),但具体实现被隐藏了。

相反,如果你点"任意一道菜",服务员可能今天给你牛排,明天给你鱼——这就是 Existential Type(存在类型):类型在运行时才确定,每次可能不同。

Swift 5.7 引入的 someany 关键字,正是为了解决这两种场景的类型安全问题。

本章适合谁

  • 想设计灵活 API 的 Swift 开发者
  • 遇到 "Protocol can only be used as a generic constraint" 编译错误的开发者
  • 想理解 Swift 类型系统进阶的工程师

你会学到什么

  • some Protocol 不透明返回类型的使用场景
  • any Protocol 存在类型的性能影响
  • some vs any 的选择策略
  • 关联类型(Associated Type)与不透明类型的关系

前置要求

  • macOS 12+ / Linux
  • Swift 6.0+
  • 已完成协议(Protocol)和泛型(Generics)章节

第一个例子

protocol Shape {
    func area() -> Double
}

struct Circle: Shape {
    let radius: Double
    func area() -> Double { .pi * radius * radius }
}

// 不透明返回类型:隐藏具体类型
func makeShape() -> some Shape {
    Circle(radius: 5)
}

let shape = makeShape()
print("Area: \(shape.area())")

编译器知道 makeShape() 返回的是 Circle,但调用者只看到 some Shape

原理解析

some Protocol(不透明类型)

some 告诉编译器:"我返回一个具体类型,但调用者不需要知道是什么"。

func makeShape() -> some Shape {
    Circle(radius: 5)  // 具体类型固定为 Circle
}

关键特性:

  • 类型固定:每次调用返回同一具体类型
  • 零性能开销:编译器在编译期知道确切类型,无需动态分发
  • 保留类型信息:可以比较两个返回值是否相等(如果具体类型支持)

any Protocol(存在类型)

any 告诉编译器:"我返回任何符合协议的类型,运行时才确定"。

func makeShapes() -> [any Shape] {
    [Circle(radius: 5), Square(side: 10)]
}

关键特性:

  • 类型擦除:数组中可以存放不同的具体类型
  • 运行时开销:需要动态分发(vtable 查找)
  • 异构集合:唯一能存储多种具体类型的方式

some vs any 对比

特性some Shapeany Shape
类型确定时机编译期运行期
性能零开销(静态分发)有开销(动态分发)
类型一致性必须返回同一类型可返回不同类型
相等比较支持(如果具体类型支持)不支持
适用场景单一返回类型异构集合

关联类型问题

protocol Container {
    associatedtype Item
    func get() -> Item
}

// ❌ 错误:Protocol with associated type cannot be used directly
func makeContainer() -> Container { ... }

// ✅ 正确:使用 some
func makeContainer() -> some Container { ... }

常见错误

错误原因解决方案
Protocol 'Shape' can only be used as a generic constraint协议有关联类型或 Self 要求使用 some Shape 替代裸协议
Function declares an opaque return type, but the return statements do not match分支返回不同类型确保所有分支返回同一具体类型
Cannot convert value of type '[any Shape]' to expected type '[some Shape]'混淆了异构和同构集合异构用 any,同构用 some

Swift vs Rust/Python 对比

SwiftRustPython
some Shapeimpl Shape无直接等价(类型提示不隐藏类型)
any Shapedyn Shape鸭子类型(动态类型语言天然支持)
编译期确定编译期确定(monomorphization)运行期确定
零性能开销零性能开销有运行时查找开销

动手练习 Level 1

创建一个 makeNumber() 函数,返回 some Numeric(定义 Numeric 协议,包含 value() -> Double)。返回 IntDouble 之一。

protocol Numeric {
    func value() -> Double
}

// 你的代码在这里

动手练习 Level 2

扩展练习:创建一个 ShapeFactory 类,有 createCircle()createSquare() 方法,都返回 some Shape

class ShapeFactory {
    func createCircle() -> some Shape {
        // 你的代码
    }
    
    func createSquare() -> some Shape {
        // 你的代码
    }
}

动手练习 Level 3

构建一个图形渲染器,使用 any Shape 存储异构图形集合,计算总面积。

点击查看答案
protocol Shape {
    func area() -> Double
    func name() -> String
}

struct Circle: Shape {
    let radius: Double
    func area() -> Double { .pi * radius * radius }
    func name() -> String { "Circle" }
}

struct Square: Shape {
    let side: Double
    func area() -> Double { side * side }
    func name() -> String { "Square" }
}

struct Renderer {
    var shapes: [any Shape] = []
    
    mutating func add(_ shape: some Shape) {
        shapes.append(shape)
    }
    
    func totalArea() -> Double {
        shapes.reduce(0) { $0 + $1.area() }
    }
    
    func render() {
        for shape in shapes {
            print("\(shape.name()): area = \(shape.area())")
        }
    }
}

var renderer = Renderer()
renderer.add(Circle(radius: 5))
renderer.add(Square(side: 10))
renderer.render()
print("Total area: \(renderer.totalArea())")

故障排查 FAQ

Q: 什么时候用 some,什么时候用 any A: 返回单一具体类型用 some(性能更好);需要存储不同类型用 any

Q: some 可以用于参数吗? A: 不可以。some 只能用于返回类型。参数使用泛型:func process<T: Shape>(_ shape: T)

Q: SwiftUI 的 body: some View 为什么不用 any View A: SwiftUI 需要知道 View 的确切类型来进行 diff 和渲染优化。any View 会丢失类型信息。

Q: any 的性能开销有多大? A: 每次方法调用需要额外的 vtable 查找。在 tight loop 中可能显著,但大多数场景可忽略。

Q: 可以把 some 转换为 any 吗? A: 可以。some 可以隐式转换为 anylet s: any Shape = makeShape()

小结

  • some Protocol 隐藏具体类型但保持类型一致性,零性能开销
  • any Protocol 允许异构集合,但有运行时动态分发开销
  • 协议有关联类型时,必须使用 some 或泛型约束
  • SwiftUI 的 some Viewsome 最重要的实际应用场景

术语表

中文英文说明
不透明类型Opaque Type隐藏具体返回类型,编译期确定
存在类型Existential Type类型擦除,运行期确定
类型擦除Type Erasure将具体类型转换为协议类型
动态分发Dynamic Dispatch运行期查找方法实现
关联类型Associated Type协议中依赖其他类型的占位符

知识检查

  1. func make() -> some Shape { Circle(radius: 1) }func make() -> Shape { ... } 有什么区别?
  2. 为什么 [some Shape] 是无效的,而 [any Shape] 是有效的?
  3. SwiftUI 的 body 属性为什么声明为 some View 而不是 any View
点击查看答案与解析
  1. some Shape 隐藏具体类型但保持类型固定;裸 Shape 在 Swift 5.7 之前会导致编译错误(协议有 Self/associatedtype 要求时),5.7+ 等同于 any Shape
  2. some 要求所有元素是同一具体类型,但编译器无法推断是什么类型;any 明确告诉编译器"类型可以不同"。
  3. SwiftUI 需要 View 的确切类型来进行树 diff 和状态管理。any View 会丢失类型信息,导致无法正确追踪状态变化。

继续学习

下一章: Unsafe Pointers — 深入底层内存操作

返回: Advance 概览

Unsafe Pointers (不安全指针)

开篇故事

想象你在银行工作。通常,你通过柜员(Swift 的安全 API)存取款——安全、有记录、不会出错。但有时你需要进入金库(Unsafe Pointer)直接操作现金——更快、更灵活,但如果操作不当,可能丢失金钱或触发警报。

Swift 的 Unsafe Pointer 就是金库钥匙:强大但危险,只在必要时使用。

本章适合谁

  • 需要与 C API 交互的 Swift 开发者
  • 追求极致性能的系统级程序员
  • 想理解 Swift 内存模型底层的工程师

你会学到什么

  • UnsafePointerUnsafeMutablePointerUnsafeRawPointer 的区别
  • MemoryLayout 获取类型大小和对齐
  • 使用 withUnsafeBufferPointer 安全地临时访问指针
  • C 函数互操作的指针传递

前置要求

  • macOS 12+ / Linux
  • Swift 6.0+
  • 已完成系统编程章节

第一个例子

var array: [Int] = [10, 20, 30, 40, 50]

// 安全地临时访问指针
array.withUnsafeBufferPointer { buffer in
    guard let base = buffer.baseAddress else { return }
    print("First element: \(base.pointee)")  // 10
    
    var sum = 0
    for i in 0..<buffer.count {
        sum += buffer[i]
    }
    print("Sum: \(sum)")  // 150
}

原理解析

指针类型家族

类型可变性类型安全用途
UnsafePointer<T>只读读取已知类型数据
UnsafeMutablePointer<T>读写修改已知类型数据
UnsafeRawPointer只读操作原始内存字节
UnsafeMutableRawPointer读写分配和操作原始内存

安全访问模式

Swift 提供 withUnsafe... 系列方法,确保指针只在闭包作用域内有效:

var value: Int32 = 42
withUnsafePointer(to: &value) { ptr in
    print("Value: \(ptr.pointee)")
}
// ptr 在此处失效,不能再使用

MemoryLayout

print("Int size: \(MemoryLayout<Int>.size)")      // 8 bytes (64-bit)
print("Int stride: \(MemoryLayout<Int>.stride)")  // 8 bytes (对齐后)
print("Int alignment: \(MemoryLayout<Int>.alignment)")  // 8 bytes

struct Point {
    var x: Double
    var y: Double
}
print("Point size: \(MemoryLayout<Point>.size)")  // 16 bytes
  • size:实际数据大小
  • stride:数组中元素的间距(含填充)
  • alignment:内存对齐要求

常见错误

错误原因解决方案
EXC_BAD_ACCESS访问已释放或无效内存确保指针在作用域内有效
unaligned access访问未对齐的内存地址使用 assumingMemoryBound(to:) 正确绑定类型
内存泄漏allocate 后未 deallocate使用 defer { ptr.deallocate() }

Swift vs Rust/Python 对比

SwiftRustPython
UnsafePointer<T>*const Tctypes.POINTER
UnsafeMutablePointer<T>*mut Tctypes.pointer
withUnsafePointer不安全块(unsafe block)无直接等价
手动管理手动管理(但编译器检查)GC 自动管理

动手练习 Level 1

使用 withUnsafePointer 获取一个 Double 变量的地址,并打印其值。

var temperature: Double = 36.6
// 你的代码:使用 withUnsafePointer 打印 temperature

动手练习 Level 2

编写一个函数 sumArray(_ array: [Int]) -> Int,使用 withUnsafeBufferPointer 和指针运算计算数组总和(不使用 for-in 循环)。

func sumArray(_ array: [Int]) -> Int {
    // 使用 baseAddress 和指针运算
}

动手练习 Level 3

实现一个简单的内存分配器:使用 UnsafeMutableRawPointer.allocate 分配内存,写入一个 Int32 值,读取后释放。

点击查看答案
func memoryAllocSample() {
    let ptr = UnsafeMutableRawPointer.allocate(
        byteCount: MemoryLayout<Int32>.size,
        alignment: MemoryLayout<Int32>.alignment
    )
    defer { ptr.deallocate() }
    
    // 绑定类型并写入
    let typedPtr = ptr.bindMemory(to: Int32.self, capacity: 1)
    typedPtr.pointee = 42
    
    // 读取
    print("Allocated value: \(typedPtr.pointee)")
}

memoryAllocSample()

故障排查 FAQ

Q: 什么时候必须用 Unsafe Pointer? A: 调用 C 函数(如 memcpyfread)、性能关键的 tight loop、自定义内存分配器。

Q: withUnsafePointer 和直接取地址有什么区别? A: Swift 没有 &variable 取地址语法。withUnsafePointer 是唯一的临时获取指针的方式,且保证作用域安全。

Q: 指针在闭包外还能用吗? A: 不能。withUnsafePointer 返回后指针失效。存储指针到外部变量会导致悬垂指针(dangling pointer)。

Q: bindMemoryassumingMemoryBound 有什么区别? A: bindMemory 改变内存的类型绑定(原始内存 → 类型化内存);assumingMemoryBound 假设内存已经绑定到某类型(不改变绑定)。

Q: Unsafe Pointer 在 Linux 上行为一致吗? A: 基本一致。但注意对齐要求可能因平台而异(ARM vs x86_64)。

小结

  • Unsafe Pointer 提供底层内存访问能力,适用于 C 互操作和性能优化
  • 始终优先使用 withUnsafe... 安全访问模式
  • MemoryLayout 是理解类型内存布局的关键工具
  • 指针操作必须手动管理生命周期,避免内存泄漏和悬垂指针

术语表

中文英文说明
不安全指针Unsafe Pointer绕过 Swift 类型安全检查的指针
内存布局Memory Layout类型在内存中的大小、间距、对齐
类型绑定Type Binding将原始内存关联到具体类型
悬垂指针Dangling Pointer指向已释放内存的指针
内存泄漏Memory Leak分配的内存未被释放

知识检查

  1. withUnsafeBufferPointerwithUnsafePointer 的使用场景有什么区别?
  2. 为什么 stride 可能大于 size
  3. bindMemory(to:capacity:)assumingMemoryBound(to:) 何时选择哪一个?
点击查看答案与解析
  1. withUnsafeBufferPointer 用于数组/集合,提供 baseAddresscountwithUnsafePointer 用于单个变量。
  2. 当类型需要内存对齐时,stride 会包含填充字节。例如 struct { UInt8; Int64 } 的 size 是 9,但 stride 是 16(8 字节对齐)。
  3. 内存是原始分配的(UnsafeMutableRawPointer.allocate)时用 bindMemory;内存已经绑定到某类型但需要临时以另一类型访问时用 assumingMemoryBound

继续学习

下一章: Swift Macros — 编译时代码生成

返回: Advance 概览

Swift Macros

开篇故事

想象你有一个机器人助手,每天早晨帮你准备咖啡。你不需要每次都告诉它"拿杯子、倒水、加咖啡粉、启动机器"——你只需要说"准备咖啡",机器人自动完成所有步骤。

Swift Macros 就是这个机器人:你写一行代码,编译器在编译时自动生成大量重复代码。

本章适合谁

  • 使用 SwiftData、Observation 等框架的开发者
  • 想减少重复代码(boilerplate)的工程师
  • 对编译时代码生成感兴趣的进阶开发者

你会学到什么

  • @attached@freestanding 宏的区别
  • 使用 -Xfrontend -dump-macro-expansions 查看宏展开
  • SwiftData @Model 宏的工作原理
  • SwiftSyntax 在宏实现中的角色

前置要求

  • macOS 12+ / Linux
  • Swift 6.0+
  • 已完成 Property Wrappers 章节

第一个例子

import Foundation

struct TodoItem: Codable, Equatable {
    var title: String
    var completed: Bool = false
    var createdAt: Date = Date()
}

var todo = TodoItem(title: "Learn Swift Macros")
print("Todo: \(todo.title), completed: \(todo.completed)")

todo.completed = true
print("Updated: \(todo.title), completed: \(todo.completed)")

CodableEquatable 本身就是编译器内置的宏——你声明遵循协议,编译器自动生成 encode/decode== 方法。

原理解析

宏的分类

类型语法示例用途
@attached@ModelSwiftData 模型附加到已有类型上,添加成员
@freestanding#selectorObjective-C 选择器独立表达式,不依赖类型

@Model 宏展开

// 你写的代码
@Model
class TodoItem {
    var title: String
    var completed: Bool = false
}

// 编译器自动生成(简化版)
class TodoItem {
    var title: String
    var completed: Bool = false
    
    // @Model 自动生成的代码
    static var all: FetchDescriptor<TodoItem> { FetchDescriptor<TodoItem>() }
    var persistentModelID: PersistentIdentifier { get }
}

查看宏展开

# 查看宏生成的代码
swift build -Xfrontend -dump-macro-expansions

SwiftSyntax

宏的实现基于 SwiftSyntax 库,它提供 AST(抽象语法树)操作能力:

// 宏实现的基本流程
// 1. 解析输入类型的 AST
// 2. 生成新的语法节点
// 3. 返回扩展后的代码

常见错误

错误原因解决方案
Macro expansion failed宏定义与使用不匹配检查宏的适用范围(class/struct)
Xcode 缓存未刷新宏定义修改后 IDE 未重新展开Clean Build Folder (⇧⌘K)
@Model 类没有无参初始化器SwiftData 要求默认构造为所有属性提供默认值

Swift vs Rust/Python 对比

SwiftRustPython
@Model (attached macro)#[derive(Model)] (proc_macro)@dataclass (decorator)
#warning (freestanding)compile_error!无直接等价(运行时装饰器)
编译时展开编译时展开运行时执行

动手练习 Level 1

创建一个遵循 CodableCustomStringConvertibleBook 结构体,包含 titleauthoryear 属性。

struct Book: Codable, CustomStringConvertible {
    // 你的代码
}

动手练习 Level 2

使用 #file#line 内置宏创建一个简单的日志函数,自动输出文件名和行号。

func log(_ message: String, file: String = #file, line: Int = #line) {
    // 你的代码
}

动手练习 Level 3

构建一个 @Observable 风格的属性包装器,当属性变化时自动打印通知。

点击查看答案
@propertyWrapper
struct Observable<Value> {
    var wrappedValue: Value {
        didSet {
            print("🔔 Property changed from \(oldValue) to \(wrappedValue)")
        }
    }
    
    init(wrappedValue: Value) {
        self.wrappedValue = wrappedValue
    }
}

class Settings {
    @Observable var volume: Int = 50
    @Observable var brightness: Double = 0.8
}

let settings = Settings()
settings.volume = 70     // 🔔 Property changed from 50 to 70
settings.brightness = 1.0 // 🔔 Property changed from 0.8 to 1.0

故障排查 FAQ

Q: 宏和属性包装器有什么区别? A: 属性包装器在运行时起作用(包装属性的 get/set);宏在编译时起作用(生成代码)。

Q: 自定义宏需要什么条件? A: 需要单独的宏目标(macro target),依赖 SwiftSyntax 库,在 Xcode 15+ 或 Swift 5.9+ 中使用。

Q: @Model@Observable 有什么区别? A: @Model 用于 SwiftData 持久化;@Observable 用于 SwiftUI 状态观察。两者都是 attached macro。

Q: 宏会影响编译速度吗? A: 会。每个宏展开都需要额外的编译时间。大型项目中大量使用宏可能增加 10-20% 编译时间。

Q: 如何调试宏生成的代码? A: 使用 swift build -Xfrontend -dump-macro-expansions 查看展开结果,或在 Xcode 中右键 → "Expand Macro"。

小结

  • Macros 在编译时生成代码,减少手写重复代码
  • @attached 附加到类型上,@freestanding 独立使用
  • CodableEquatable@Model 都是宏的实际应用
  • 使用 -dump-macro-expansions 可以查看宏生成的代码

术语表

中文英文说明
Macro编译时代码生成机制
附加宏Attached Macro附加到已有类型上的宏
独立宏Freestanding Macro不依赖类型的独立表达式
抽象语法树AST代码的结构化表示
宏展开Macro Expansion编译器将宏替换为实际代码的过程

知识检查

  1. @Model 是 attached macro 还是 freestanding macro?为什么?
  2. 如何查看 Swift 宏展开后的代码?
  3. 宏和属性包装器的执行时机有什么不同?
点击查看答案与解析
  1. @Model 是 attached macro,因为它附加到 class 上,自动生成 persistentModelID 等成员。
  2. 使用 swift build -Xfrontend -dump-macro-expansions 或在 Xcode 中右键选择 "Expand Macro"。
  3. 宏在编译时执行(生成代码);属性包装器在运行时执行(包装属性访问)。

继续学习

下一章: Result Builders — 构建声明式 DSL

返回: Advance 概览

Result Builders

开篇故事

想象你在搭积木。每次拿起一块积木,说"这块放这里",最终搭成一座城堡。Swift 的 Result Builder 就是这种"声明式搭建"的机制——你列出组件,Builder 自动组合它们。

SwiftUI 的 VStack { Text("Hello"); Button("Tap me") { } } 就是 Result Builder 的经典应用。

本章适合谁

  • 使用 SwiftUI 想理解底层原理的开发者
  • 想创建声明式 DSL(领域特定语言)的工程师
  • 对 Swift 高级语法特性感兴趣的进阶程序员

你会学到什么

  • @resultBuilder 属性的使用方法
  • buildBlockbuildOptionalbuildEither 的作用
  • 理解 SwiftUI ViewBuilder 的工作原理
  • 创建自定义 DSL

前置要求

  • macOS 12+ / Linux
  • Swift 6.0+
  • 已完成 Macros 章节

第一个例子

@resultBuilder
struct StringBuilder {
    static func buildBlock(_ components: String...) -> String {
        components.joined(separator: "\n")
    }
}

func buildString(@StringBuilder builder: () -> String) -> String {
    builder()
}

let result = buildString {
    "Line 1: Hello"
    "Line 2: World"
    "Line 3: Swift"
}
print(result)
// Line 1: Hello
// Line 2: World
// Line 3: Swift

原理解析

核心方法

方法作用对应语法
buildBlock组合多个组件普通语句序列
buildOptional处理可选组件if 语句
buildEither(first:) / buildEither(second:)处理分支if/else 语句
buildArray处理循环for 循环
buildExpression转换单个表达式每个输入值
buildFinalResult最终转换返回值

条件分支支持

@resultBuilder
struct StringBuilder {
    static func buildBlock(_ components: String...) -> String {
        components.joined(separator: "\n")
    }
    
    static func buildOptional(_ component: String?) -> String {
        component ?? ""
    }
    
    static func buildEither(first: String) -> String { first }
    static func buildEither(second: String) -> String { second }
}

let result = buildString {
    "Always included"
    if true {
        "Conditionally included"
    }
    if false {
        "Not included"
    } else {
        "Else branch"
    }
}

SwiftUI ViewBuilder 原理

// SwiftUI 的 VStack 内部使用 ViewBuilder
VStack {
    Text("Hello")       // buildExpression
    Button("Tap") { }   // buildExpression
    if showExtra {      // buildOptional / buildEither
        Text("Extra")
    }
}
// ViewBuilder.buildBlock 组合所有 View

常见错误

错误原因解决方案
Static member 'buildBlock' cannot be used on instance方法必须是 static确保所有 build 方法都是 static
Type 'X' has no member 'buildEither'缺少分支处理方法添加 buildEither(first:)buildEither(second:)
嵌套条件编译失败缺少 buildOptional 嵌套支持确保 buildOptional 返回类型与 buildBlock 一致

Swift vs Rust/Python 对比

SwiftRustPython
@resultBuilder宏(macro_rules!装饰器(@decorator
buildBlock宏展开__enter__/__exit__ (context manager)
编译时组合编译时展开运行时执行

动手练习 Level 1

创建一个 HTMLBuilder,将多个 HTML 标签字符串包裹在 <div> 中。

@resultBuilder
struct HTMLBuilder {
    static func buildBlock(_ components: String...) -> String {
        // 你的代码
    }
}

动手练习 Level 2

扩展 HTMLBuilder,支持 if 条件(buildOptional)和 if/else 分支(buildEither)。

func render(@HTMLBuilder content: () -> String) -> String {
    "<html><body>" + content() + "</body></html>"
}

动手练习 Level 3

构建一个简易测试框架 DSL,支持 test 块和 expect 断言。

点击查看答案
@resultBuilder
struct TestBuilder {
    static func buildBlock(_ components: [String]...) -> [String] {
        components.flatMap { $0 }
    }
    
    static func buildOptional(_ component: [String]?) -> [String] {
        component ?? []
    }
}

func testSuite(@TestBuilder tests: () -> [String]) {
    let results = tests()
    for result in results {
        print(result)
    }
}

func test(_ name: String, _ body: () -> Bool) -> [String] {
    let passed = body()
    return ["\(passed ? "✅" : "❌") \(name)"]
}

testSuite {
    test("Addition") { 1 + 1 == 2 }
    test("String") { "hello".count == 5 }
}

故障排查 FAQ

Q: Result Builder 和函数式编程的 reduce 有什么区别? A: Result Builder 在编译时转换语法结构(DSL),reduce 在运行时聚合数据。

Q: 为什么 SwiftUI 要用 Result Builder 而不是数组? A: DSL 语法更自然(不需要 [Text("A"), Button("B")]),且支持 if/for 等控制流。

Q: buildExpression 是必须的吗? A: 不是。如果输入类型和输出类型一致,可以省略。它用于在组合前转换单个表达式。

Q: Result Builder 可以嵌套吗? A: 可以。一个 builder 方法可以调用另一个 builder,实现嵌套 DSL。

小结

  • @resultBuilder 将声明式语法转换为函数调用
  • buildBlock 组合、buildOptional 处理 ifbuildEither 处理 if/else
  • SwiftUI 的 ViewBuilder 是 Result Builder 最重要的实际应用
  • 自定义 Builder 可以创建领域特定语言(DSL)

术语表

中文英文说明
结果构建器Result Builder将声明式语法转换为函数调用的机制
声明式Declarative描述"做什么"而非"怎么做"
领域特定语言DSL针对特定领域设计的简洁语法
构建块Build Block组合多个组件的核心方法

知识检查

  1. buildOptionalbuildEither 分别对应什么 Swift 语法?
  2. 为什么 buildBlock 的参数是 ...(variadic)?
  3. SwiftUI 的 ViewBuilder 如何处理 ForEach 循环?
点击查看答案与解析
  1. buildOptional 对应 if(无条件分支),buildEither 对应 if/else(二选一分支)。
  2. Variadic 参数允许接收任意数量的组件,实现灵活的组件组合。
  3. ViewBuilder 通过 buildArray 方法处理 ForEach,将多个 View 数组元素组合为单一 View。

继续学习

下一章: Mirror Reflection — 运行时类型检查

返回: Advance 概览

Mirror Reflection (Mirror 反射)

开篇故事

想象你有一面魔镜,照任何物体都能告诉你它的组成部分:这个杯子有把手、杯身、杯底。Swift 的 Mirror 就是这面魔镜——它能在运行时告诉你任何类型的结构。

但魔镜只能"看",不能"改"。你可以知道属性名和值,但不能修改它们。

本章适合谁

  • 需要调试和日志功能的 Swift 开发者
  • 想实现序列化/反序列化的工程师
  • 对 Swift 运行时类型检查感兴趣的进阶程序员

你会学到什么

  • Mirror(reflecting:) 的基本用法
  • children 遍历类型属性
  • displayStyle 区分 struct/class/enum/tuple
  • 反射的性能限制和适用场景

前置要求

  • macOS 12+ / Linux
  • Swift 6.0+
  • 已完成 Result Builders 章节

第一个例子

struct User {
    let name: String
    var age: Int
    var email: String?
}

let user = User(name: "Alice", age: 30, email: "alice@example.com")

let mirror = Mirror(reflecting: user)
print("Type: \(mirror.displayStyle ?? .unknown)")  // struct

for child in mirror.children {
    if let label = child.label {
        print("  \(label): \(child.value)")
    }
}
// name: Alice
// age: 30
// email: alice@example.com

原理解析

Mirror 核心属性

属性类型说明
childrenChildren子元素集合(label + value)
displayStyleDisplayStyle?类型类别(struct/class/enum/tuple/collection/optional)
subjectTypeAny.Type被反射的类型

支持 displayStyle

let status = Status.active
let statusMirror = Mirror(reflecting: status)
print("Enum: \(statusMirror.displayStyle)")  // Optional(Swift.Mirror.DisplayStyle.enum)

let tuple = (x: 10, y: 20)
let tupleMirror = Mirror(reflecting: tuple)
print("Tuple: \(tupleMirror.displayStyle)")  // Optional(Swift.Mirror.DisplayStyle.tuple)

反射的限制

  • 只读:无法通过 Mirror 修改属性值
  • 性能:反射比直接访问慢 10-100 倍
  • 不递归children 只返回直接子元素,不深入嵌套

常见错误

错误原因解决方案
child.label 为 nil某些类型(如 tuple)的 label 可能为空使用 child.label ?? "unknown"
反射大对象性能差Mirror 遍历所有属性限制反射深度或使用自定义序列化
尝试修改反射值Mirror 是只读的使用 KeyPath 或自定义协议

Swift vs Rust/Python 对比

SwiftRustPython
Mirror(reflecting:)std::any::type_nameinspect.getmembers()
只读反射有限反射(无运行时类型信息)完整反射(可读写)
编译期类型信息编译期擦除运行时完整类型信息

动手练习 Level 1

使用 Mirror 反射一个 Point(x: 3, y: 4) 结构体,打印所有属性名和值。

struct Point {
    let x: Int
    let y: Int
}
// 你的代码

动手练习 Level 2

创建一个 describe(_ object: Any) -> String 函数,使用 Mirror 生成对象的可读描述。

func describe(_ object: Any) -> String {
    // 使用 Mirror 生成类似 "TypeName(prop1: value1, prop2: value2)" 的字符串
}

动手练习 Level 3

构建一个简单的 JSON 序列化器,使用 Mirror 将任意结构体转换为 JSON 字符串(仅支持 String、Int、Double、Bool)。

点击查看答案
func toJson(_ object: Any) -> String {
    let mirror = Mirror(reflecting: object)
    var pairs: [String] = []
    
    for child in mirror.children {
        guard let label = child.label else { continue }
        
        let valueStr: String
        switch child.value {
        case let s as String:
            valueStr = "\"\(s)\""
        case let i as Int:
            valueStr = "\(i)"
        case let d as Double:
            valueStr = "\(d)"
        case let b as Bool:
            valueStr = b ? "true" : "false"
        default:
            valueStr = "\"\(child.value)\""
        }
        pairs.append("\"\(label)\": \(valueStr)")
    }
    
    return "{\(pairs.joined(separator: ", "))}"
}

struct Product {
    let name: String
    let price: Double
    let inStock: Bool
}

let product = Product(name: "Swift Book", price: 29.99, inStock: true)
print(toJson(product))
// {"name": "Swift Book", "price": 29.99, "inStock": true}

故障排查 FAQ

Q: Mirror 能修改属性值吗? A: 不能。Mirror 是只读的。如果需要读写,使用 WritableKeyPath 或自定义协议。

Q: 反射的性能影响有多大? A: 比直接属性访问慢 10-100 倍。在 hot path 中避免使用。调试和序列化场景可接受。

Q: Mirror 能反射 private 属性吗? A: 可以。Mirror 不受访问控制限制,能看到所有属性。

Q: 如何限制反射深度(避免无限递归)? A: 在递归函数中添加 depth 参数,超过阈值时停止。

Q: Mirror 支持泛型类型吗? A: 支持。Mirror(reflecting:) 接受 Any,泛型实例会被正确反射。

小结

  • Mirror 提供运行时类型检查能力,适用于调试和序列化
  • children 遍历属性,displayStyle 区分类型类别
  • Mirror 是只读的,性能有限,不适合 hot path
  • Python 的 inspect 提供更完整的反射(可读写),Swift 更注重安全

术语表

中文英文说明
反射Reflection运行时检查类型结构的能力
显示样式Display Style类型的外观类别
子元素Children类型的直接属性集合
只读Read-Only只能读取不能修改

知识检查

  1. 为什么 Mirror 的 children 不递归返回嵌套属性?
  2. displayStyle 有哪些可能的值?
  3. 在什么场景下应该避免使用 Mirror?
点击查看答案与解析
  1. 防止无限递归(如循环引用)和性能问题。需要递归时手动调用 Mirror(reflecting: child.value)
  2. .struct.class.enum.tuple.collection.optional.set.dictionary
  3. 性能敏感的 hot path、需要修改属性值的场景、大型对象的完整序列化(应使用 Codable)。

继续学习

下一章: 返回 Advance 阶段回顾 — 测试你学到的知识

返回: Advance 概览

Swift 术语表

本表汇总基础部分所有英文术语及对应的中文翻译。

基础语法

English中文说明
Constant常量使用 let 声明,不可修改
Variable变量使用 var 声明,可以修改
Type Inference类型推断编译器自动判断变量类型
String Interpolation字符串插值在字符串中嵌入表达式 \(...)
Shadowing遮蔽在嵌套作用域重新声明同名变量

数据类型

English中文说明
Optional可选类型? 表示,值可为空(nil)
Optional Binding可选绑定使用 if let 安全解包
Array数组有序集合,允许重复
Set集合无序集合,不允许重复
Dictionary字典键值对集合
Tuple元组组合多个值为一个复合值

控制流

English中文说明
Condition条件if/else 中的布尔表达式
Exhaustive穷举switch 必须覆盖所有可能情况
Pattern Matching模式匹配在 switch/case 中匹配值的结构
Guard StatementGuard 语句用于提前退出,条件不满足时执行

函数与闭包

English中文说明
Parameter Label参数标签调用时使用的参数名称
Variadic Parameter可变参数接受零个或多个相同类型的值
Inout Parameter输入输出参数允许函数修改传入的参数
Closure闭包可以捕获上下文的匿名函数
Escaping Closure逃逸闭包在函数返回后仍被调用的闭包(@escaping)
Trailing Closure尾随闭包函数最后一个参数为闭包时的简化语法

类型系统

English中文说明
Value Type值类型赋值时复制(Struct, Enum, Tuple)
Reference Type引用类型赋值时引用同一对象(Class)
Property Observer属性观察器willSet / didSet,在属性修改前后执行
Mutating Method可变方法值类型中修改自身属性的方法
Inheritance继承子类获得父类的属性和方法
ARC自动引用计数Automatic Reference Counting,自动管理内存
Weak Reference弱引用不增加引用计数的引用

协议与泛型

English中文说明
Protocol协议定义接口规范,类型必须实现
Protocol Extension协议扩展为协议提供默认实现
Associated Type关联类型在协议中定义的占位类型
Protocol Composition协议组合同时遵循多个协议(ProtocolA & ProtocolB
Opaque Type不透明类型使用 some 隐藏具体类型
Generic泛型编写可适用于多种类型的代码
Type Constraint类型约束限制泛型参数必须遵循的协议或基类
Where ClauseWhere 子句定义额外的类型约束条件

错误处理

English中文说明
Throwing Function抛出函数使用 throws 标记的函数
Defer Block延迟执行块在作用域退出时执行
Result Type结果类型enum Result<Success, Failure> 包装结果
Catch捕获处理抛出的错误

并发编程

English中文说明
Async/Await异步/等待Swift 语言级并发语法
Task任务异步执行单元
TaskGroup任务组动态创建的并发任务集合
Actor参与者保护共享状态免受数据竞争的引用类型
Actor Isolation参与者隔离确保对 Actor 内部状态的访问是串行的
Main Actor主参与者与主线程关联的 Actor(@MainActor)
Sendable可发送可安全跨越并发边界的类型协议
Data Race数据竞争多个线程同时访问共享数据且至少一个在写入
Strict Concurrency严格并发Swift 6.0 引入的编译时数据竞争检测
AsyncSequence异步序列异步生成元素的序列

说明: 术语翻译参考 Apple 官方 Swift 中文文档和社区通用译法。

阶段复习:基础部分

📖 复习目标

恭喜完成基础部分 12 章的学习!本章帮助你巩固所有关键概念,为高级进阶做好准备。


✅ 自检清单

完成以下每项,全部打勾后即可进入高级部分:

变量与表达式

  • 能用 letvar 正确声明变量
  • 理解 Swift 默认不可变的设计哲学
  • 能使用字符串插值 \(...) 构建动态文本

基础数据类型

  • 能区分 Int/UInt 家族、Float/Double
  • 能创建和操作 Array、Set、Dictionary
  • 理解可选类型 ? 和可选绑定 if let

控制流

  • 能使用 if/else、switch/case 编写条件逻辑
  • 理解 switch 的穷举性要求(不需要 default)
  • 能使用 guard 进行早期退出
  • 理解 for-in 遍历范围、数组、字典

函数

  • 能定义带外部参数标签和内部参数的函数
  • 理解默认参数值、可变参数、inout 参数
  • 能将函数作为参数和返回值

枚举

  • 能用 enum 定义多种状态
  • 理解关联值(associated values)和原始值(raw values)
  • 能使用 switch 模式匹配枚举

结构体

  • 能定义结构体并理解值类型语义
  • 理解属性观察器(willSet/didSet)
  • 知道 mutating 方法的用途

类与对象

  • 能定义类、实现继承
  • 理解引用类型 vs 值类型的区别
  • 理解 ARC(自动引用计数)和循环引用问题

协议

  • 能定义协议并让类型实现协议
  • 理解协议扩展的默认实现
  • 理解 Swift 的"面向协议编程"(POP)设计理念

泛型

  • 能编写泛型函数和泛型类型
  • 理解类型约束和 where 子句
  • 知道何时使用泛型 vs 协议

错误处理

  • 能使用 do/catch/try 处理错误
  • 理解 throws、try?、try! 的区别
  • 知道 defer 块的执行时机
  • 理解 Result 类型

闭包

  • 能用 {} 语法定义闭包
  • 理解尾随闭包语法
  • 理解逃逸闭包 (@escaping)和非逃逸闭包的区别

并发编程

  • 能使用 async/await 编写异步代码
  • 理解 Actor 和 @MainActor 的隔离机制
  • 理解 Sendable 协议的作用(Swift 6.0 Strict Concurrency)
  • 能使用 Task 和 TaskGroup 编写并发代码

🧪 综合练习

练习 1:通讯录管理

使用结构体、枚举、可选类型和数组,实现一个简易通讯录:

  • 联系人包含姓名、电话(可选)、邮箱(可选)
  • 支持添加、删除、查找联系人
  • 使用枚举区分联系方式(电话/邮箱/地址)

练习 2:异步数据获取

使用 async/await 和错误处理,模拟网络请求:

  • 定义错误类型(网络错误、解析错误、超时)
  • 使用 Result 类型或 throws 处理结果
  • 使用 Task 并发获取多条数据

➡️ 下一步

基础部分完成后,继续学习:

  • 高级进阶: JSON 处理、文件操作、系统服务、异步编程、环境配置
  • 实战精选: 第三方库集成示例

你已经掌握了 Swift 的核心语法!继续前进! 🚀

高级进阶术语表

本文档收录 Swift 高级进阶部分的常用术语,供读者查阅参考。


A-E

中文英文说明
数组Array有序集合,支持索引访问
异步async异步函数声明关键字
ActorActorSwift 并发模型中的隔离单元,保证数据安全
编码Encoding将 Swift 类型转换为外部格式(如 JSON)
CodableCodable编码和解码协议的组合 (Encodable + Decodable)
CodingKeysCodingKeys自定义 JSON 键名映射的枚举
容器ContainerSwiftData 中管理数据模型实例的对象
缓存目录Caches Directory存储临时缓存文件,系统可能清理
文档目录Documents Directory存储用户文档,iTunes 会备份
动态成员查找Dynamic Member Lookup编译时动态访问属性的特性
EventLoopEventLoopSwiftNIO 事件循环,单线程管理多连接
Echo ServerEcho Server收到消息原样返回的测试服务器

F-I

中文英文说明
描述符FetchDescriptorSwiftData 查询配置对象
文件管理器FileManagerFoundation 文件系统操作类
强制解包Force Unwrap使用 ! 强制获取可选值(危险操作)
FutureEventLoopFutureSwiftNIO 异步结果容器
@Model@ModelSwiftData 数据模型宏
ModelActorModelActorSwiftData 并发安全的 Actor 模式
ModelContainerModelContainerSwiftData 数据库容器
ModelContextModelContextSwiftData 操作上下文
迁移Migration数据模型变更时的迁移策略
不可变性Immutability常量声明后不可修改的特性
InboundHandlerInboundHandlerSwiftNIO 入站数据处理器
NIOLoopBoundBoxNIOLoopBoundBox跨 Actor 安全访问 EventLoop-bound 值

J-P

中文英文说明
JSON 解码器JSONDecoderCodable 协议的 JSON 解码工具
JSON 编码器JSONEncoderCodable 协议的 JSON 编码工具
JSON 序列化JSONSerializationFoundation 传统 JSON 解析工具
谓词Predicate数据库查询过滤条件表达式
#Predicate#PredicateSwiftData 查询条件宏
进程信息ProcessInfo获取系统环境变量的单例
进程ProcessFoundation 进程执行类
属性包装器Property Wrapper包装属性访问的自定义类型
PipelineChannelPipelineSwiftNIO Channel 处理器链
PromiseEventLoopPromiseFuture 的写入端

Q-T

中文英文说明
查询QuerySwiftData SwiftUI 数据请求
RAIIRAII资源获取即初始化,用于自动清理
关系RelationshipSwiftData 模型间的关联关系
排序描述符SortDescriptor查询结果的排序配置
SendableSendable跨并发边界安全传递的协议
流式读取Streaming Read异步逐行读取大文件
TaskTaskSwift 并发任务的执行单元
临时文件Temporary File系统自动清理的短期文件
临时目录Temporary Directory存放临时文件的系统目录
SignalSignal操作系统发送给进程的通知
SIGINTSIGINTCtrl+C 中断信号(可捕获)
SIGTERMSIGTERM优雅终止信号(可捕获)
ServerBootstrapServerBootstrapSwiftNIO 服务器启动器
setUp/tearDownsetUp/tearDownXCTestCase 测试生命周期方法

U-Z

中文英文说明
URLURL文件路径或网络地址的表示
值类型Value Typestruct、enum 等复制语义的类型
等待await异步函数等待结果的运算符
SwiftyJSONSwiftyJSON第三方 JSON 解析库,简化访问
XCTestXCTestSwift 内置测试框架
XCTestCaseXCTestCaseXCTest 测试类基类
XCTAssertEqualXCTAssertEqualXCTest 相等断言
XCTAssertThrowsErrorXCTAssertThrowsErrorXCTest 抛出错误断言
ByteBufferByteBufferSwiftNIO 高效字节容器,零拷贝设计

环境配置术语

中文英文说明
环境变量Environment Variable系统级配置参数
.env 文件.env file项目级环境配置文件
dotenvdotenv加载 .env 文件的工具/库
API 密钥API Key第三方服务的访问凭证

SwiftData 术语

中文英文说明
模型宏@Model macro将 class 转换为持久化模型的宏
持久标识符PersistentIdentifierSwiftData 对象的唯一 ID
内存模式In-Memory Mode不写入磁盘的数据库配置
SQLiteSQLiteSwiftData 默认的存储后端
级联删除Cascade Delete关系对象的自动删除规则

SwiftNIO 术语

中文英文说明
通道ChannelSwiftNIO 网络连接抽象
事件循环组EventLoopGroupSwiftNIO 多线程事件循环管理器
非阻塞Non-blocking不等待 I/O 完成,立即返回
桥接BridgingFuture 与 async/await 的连接方式
ContinuationContinuationasync/await 的底层挂起/恢复机制
EventLoop 绑定EventLoop-bound值绑定到特定 EventLoop,只能在其上操作
零拷贝Zero-copyByteBuffer slice 不复制数据的设计

系统编程术语

中文英文说明
子进程Child process由父进程启动的进程
状态码Termination status进程退出返回的数值(0 成功)
管道Pipe进程间通信的数据流
捕获Catch接收并处理 Signal
优雅关闭Graceful shutdown先清理资源再退出
沙箱SandboxmacOS 的应用隔离目录
stdoutstdout标准输出流
stderrstderr标准错误流

测试框架术语

中文英文说明
测试类XCTestCase包含多个测试方法的类
测试方法Test method以 test 开头的函数
断言Assertion检查预期结果的语句
生命周期LifecyclesetUp → test → tearDown 的执行顺序
异步测试Async test标记 async 的测试方法
性能测试Performance testmeasure {} 测量执行时间
测试隔离Test isolation每个测试独立运行,不影响其他
测试过滤Test filter--filter 只运行部分测试

平台术语

中文英文说明
macOS 14+macOS 14+SwiftData 所需最低版本
macOS 12+macOS 12+FileManager async APIs 所需版本
LinuxLinuxSwift 支持的平台,部分特性受限
应用支持目录Application Support Directory存储应用配置和数据库的目录
跨平台Cross-platformmacOS 和 Linux 双平台支持
DarwinDarwinmacOS 内核,信号处理 API
GlibcGlibcLinux C 库,POSIX API

Vapor Web 框架术语

中文英文说明
路由RouteURL 路径到处理函数的映射
中间件Middleware拦截请求/响应的处理器链
内容协议ContentVapor 的 JSON 编解码协议
应用实例ApplicationVapor 服务器的核心对象
异步中间件AsyncMiddleware支持 async/await 的中间件
异步响应器AsyncResponder中间件链中的下一个处理器
参数提取Parameters从 URL 路径中提取动态值
Fluent ORMFluentVapor 的数据库 ORM 框架

GRDB 数据库术语

中文英文说明
数据库队列DatabaseQueue单线程 SQLite 连接管理器
数据库池DatabasePool多线程 SQLite 连接池
可获取记录FetchableRecord从数据库行解码为 Swift 类型的协议
可持久化记录PersistableRecord将 Swift 类型写入数据库的协议
查询接口QueryInterface类型安全的 SQL 查询 DSL
关联Association表之间的关系定义(BelongsTo/HasMany)
事务Transaction原子性的数据库操作组
WAL 模式WAL ModeWrite-Ahead Logging,提高并发性能

并发深入术语

中文英文说明
参与者ActorSwift 并发模型中的隔离单元,保证数据安全
隔离域Isolation DomainActor 保护的状态范围
非隔离Nonisolated不受 Actor 隔离限制的方法
可发送Sendable可安全跨并发边界传递的类型
严格并发Strict ConcurrencySwift 6 的编译时并发安全检查
闭包捕获Closure Capture闭包引用外部变量的行为
未检查发送@unchecked Sendable手动声明 Sendable(编译器不验证)

Swift 高级特性术语

中文英文说明
属性包装器Property Wrapper包装属性 get/set 的自定义类型
包装值Wrapped Value属性包装器存储的实际值
投影值Projected Value属性包装器暴露的额外接口($ 前缀)
自动引用计数ARCSwift 的内存管理机制
强引用Strong Reference增加对象引用计数的引用
弱引用Weak Reference不增加引用计数,自动置 nil
无主引用Unowned Reference不增加引用计数,不会自动置 nil
循环引用Retain Cycle两个对象互相强引用导致无法释放
不透明类型Opaque Typesome Protocol,隐藏具体返回类型
存在类型Existential Typeany Protocol,运行时类型擦除
类型擦除Type Erasure将具体类型转换为协议类型
动态分发Dynamic Dispatch运行期查找方法实现
不安全指针Unsafe Pointer绕过 Swift 类型安全检查的指针
内存布局Memory Layout类型在内存中的大小、间距、对齐
悬垂指针Dangling Pointer指向已释放内存的指针
Macro编译时代码生成机制
附加宏Attached Macro附加到已有类型上的宏(如 @Model)
独立宏Freestanding Macro不依赖类型的独立表达式(如 #warning)
宏展开Macro Expansion编译器将宏替换为实际代码的过程
结果构建器Result Builder将声明式语法转换为函数调用的机制
声明式Declarative描述"做什么"而非"怎么做"
构建块Build Block组合多个组件的核心方法
反射Reflection运行时检查类型结构的能力
显示样式Display StyleMirror 中类型的外观类别
只读Read-Only只能读取不能修改

返回: 高级进阶

阶段复习:高级进阶

完成高级进阶部分的八个章节后,让我们通过本复习章节巩固所学知识。


📋 知识回顾

Phase 1: 数据处理与持久化

1. JSON 处理

核心概念:

  • JSONSerialization: Foundation 传统方法,返回 Any 类型,需要手动转换
  • JSONDecoder/Codable: 类型安全的现代方法,自动映射到 Swift 类型
  • SwiftyJSON: 第三方库,动态成员查找简化深层访问
  • CodingKeys: 自定义 JSON 键名与 Swift 属性名的映射

关键要点:

  • Codable 是 Encodable + Decodable 的组合
  • 避免使用 try! 强制解包,使用 do/catch/try 处理错误
  • 可选字段使用 decodeIfPresent 或可选类型属性

2. 文件操作

核心概念:

  • FileManager: 文件系统操作的核心类
  • 标准目录: Documents(用户数据)、Caches(缓存)、Temp(临时)、Application Support(配置)
  • TemporaryFile: RAII 模式,deinit 自动清理
  • AsyncLineSequence: 异步流式读取大文件

关键要点:

  • Documents 和 Application Support 会备份,Caches 和 Temp 不备份
  • 使用 FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) 获取路径
  • Linux 平台无 Documents/Caches 概念,使用 currentDirectoryPath
  • @available(macOS 12.0, *) 检查 async API 可用性

3. SwiftData 持久化

核心概念:

  • @Model: 数据模型宏,自动生成持久化代码
  • ModelContainer: 数据库容器,配置存储位置
  • ModelContext: 操作上下文,执行 CRUD
  • FetchDescriptor: 查询配置,包含 predicate 和 sort
  • #Predicate: 查询条件宏,编译期检查
  • @ModelActor: Actor 模式,后台线程安全操作

关键要点:

  • SwiftData requires macOS 14.0+
  • 必须提供初始化器(@Model 要求)
  • 跨 Actor 传递 PersistentIdentifier,不能传递模型对象
  • 使用 ModelConfiguration(isStoredInMemoryOnly: true) 测试模式

4. 环境配置

核心概念:

  • ProcessInfo.processInfo.environment: 读取系统环境变量
  • swift-dotenv: 加载 .env 文件
  • Dotenv.configure(): 初始化并加载配置
  • 动态成员查找: Dotenv.apiKey 直接访问

关键要点:

  • API 密钥等敏感信息不要硬编码
  • .env 文件不要提交到 Git(添加到 .gitignore
  • 开发/生产环境切换使用不同的 .env 文件
  • 使用 Dotenv["KEY"]Dotenv.key?.stringValue 访问值

Phase 2: 网络与系统编程

5. SwiftNIO 网络基础

核心概念:

  • EventLoop: 单线程事件循环,管理多个网络连接
  • Channel: 网络连接抽象,由 Pipeline 和 Handler 组成
  • ChannelHandler: 入站/出站数据处理单元
  • ByteBuffer: 高效字节容器,零拷贝设计
  • ServerBootstrap: TCP 服务器启动器

关键要点:

  • 一个 EventLoop 可以管理数千连接,避免线程爆炸
  • ChannelHandler 分为 InboundHandler 和 OutboundHandler
  • ByteBuffer 的 slice 操作不复制数据(零拷贝)
  • 不要在 EventLoop 线程使用 Thread.sleep(会阻塞所有连接)

6. SwiftNIO async/await 集成

核心概念:

  • Future → async 桥接: withCheckedContinuation 转换回调式 API
  • NIOLoopBoundBox: 跨 Actor 安全访问 EventLoop-bound 值
  • NIOAsyncChannel: SwiftNIO 2.40+ 原生 async/await API
  • Sendable 约束: Channel 不是 Sendable,需要包装

关键要点:

  • 不要在 async 函数中调用 future.wait()(会阻塞底层线程)
  • NIOLoopBoundBox 保证在正确的 EventLoop 上操作 Channel
  • NIOAsyncChannel 提供 AsyncSequence 接口,简化代码
  • Actor 内部不能用 Channel,需要用 NIOLoopBoundBox 包装

7. 系统编程

核心概念:

  • Process: Foundation 进程执行类,捕获 stdout/stderr
  • Pipe: 进程间通信的数据流
  • Signal: SIGINT/SIGTERM 捕获,优雅关闭
  • 跨平台路径: macOS Documents/Caches vs Linux HOME/tmp

关键要点:

  • Process.run() 非阻塞,waitUntilExit() 阻塞等待
  • 使用 Pipe 捕获子进程输出
  • Linux 没有 Documents 目录,使用 HOME 或 currentDirectoryPath
  • Signal Handler 应只设置标志,主循环处理清理逻辑

8. 测试框架

核心概念:

  • XCTestCase: 测试类基类,setUp/tearDown 生命周期
  • 断言方法: XCTAssertEqual、XCTAssertTrue、XCTAssertThrowsError 等
  • async 测试: 测试方法标记 async,使用 await
  • measure {}: 性能测试,测量执行时间

关键要点:

  • 测试方法必须以 test 开头
  • setUp 在每个测试前执行,tearDown 在每个测试后执行
  • @testable import 可访问 internal 成员
  • swift test --filter 只运行部分测试

🧪 综合练习

练习 1: JSON + 文件 + SwiftData

任务: 编写一个应用,读取 JSON API 响应,解析数据,写入临时文件,并存储到 SwiftData。

步骤:

  1. 使用 JSONDecoder 解析 API 返回的用户 JSON
  2. 将解析结果写入临时文件(用于调试)
  3. 使用 @Model 定义 User 模型
  4. ModelContext.insert() 存储到数据库
点击查看提示
// 1. 解析 JSON
let decoder = JSONDecoder()
let users = try decoder.decode([User].self, from: jsonData)

// 2. 写入临时文件
let tempFile = try TemporaryFile(content: String(data: jsonData, encoding: .utf8)!)

// 3. 定义 @Model
@Model
class User {
    var id: UUID
    var name: String
    var email: String
    
    init(name: String, email: String) {
        self.id = UUID()
        self.name = name
        self.email = email
    }
}

// 4. 存储到数据库
for user in users {
    modelContext.insert(user)
}

练习 2: SwiftNIO Echo Server + Process 测试

任务: 创建 SwiftNIO Echo Server,使用 Process 执行 telnet 测试连接。

步骤:

  1. ServerBootstrap 创建 TCP 服务器监听 8080
  2. EchoHandler 收到消息原样返回
  3. Process 执行 telnet 连接测试
点击查看提示
// 1. 创建 Echo Server
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let bootstrap = ServerBootstrap(group: group)
    .childChannelInitializer { channel in
        channel.pipeline.addHandler(EchoHandler())
    }
let server = try bootstrap.bind(host: "127.0.0.1", port: 8080).wait()

// 2. EchoHandler 实现
final class EchoHandler: ChannelInboundHandler {
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        context.write(data, promise: nil)
    }
    func channelReadComplete(context: ChannelHandlerContext) {
        context.flush()
    }
}

// 3. Process 测试
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/telnet")
process.arguments = ["127.0.0.1", "8080"]
process.run()

练习 3: XCTest 测试覆盖

任务: 为 Process 执行函数编写 XCTest 测试。

步骤:

  1. 测试正常命令执行(如 /bin/ls)
  2. 测试错误命令路径
  3. 测试输出捕获
  4. async 测试异步执行
点击查看提示
final class ProcessTests: XCTestCase {
    
    func testExecuteLs() {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: "/bin/ls")
        process.arguments = ["-la"]
        
        process.run()
        process.waitUntilExit()
        
        XCTAssertEqual(process.terminationStatus, 0)
    }
    
    func testCaptureOutput() {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: "/bin/echo")
        process.arguments = ["test"]
        
        let pipe = Pipe()
        process.standardOutput = pipe
        
        process.run()
        process.waitUntilExit()
        
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(data: data, encoding: .utf8) ?? ""
        
        XCTAssertTrue(output.contains("test"))
    }
    
    func testAsyncExecute() async throws {
        let result = await withCheckedContinuation { continuation in
            let process = Process()
            process.executableURL = URL(fileURLWithPath: "/bin/ls")
            process.run()
            process.waitUntilExit()
            continuation.resume(returning: process.terminationStatus)
        }
        XCTAssertEqual(result, 0)
    }
}

✅ 知识检查

Phase 1 问题

问题 1: Swift 中解析 JSON 有哪三种主要方法?各自的优缺点是什么?

点击查看答案
方法优点缺点
JSONSerializationFoundation 内置,无需额外依赖返回 Any,需要手动类型转换,易出错
JSONDecoder/Codable类型安全,编译期检查,自动映射需要定义 Codable 类型,灵活性较低
SwiftyJSON动态访问,深层嵌套简化,容错性强第三方依赖,性能略低

问题 2: FileManager 的 Documents、Caches、Temp 目录有什么区别?

点击查看答案
目录用途备份清理
Documents用户文档、重要数据iTunes 备份不会自动清理
Caches缓存数据、下载临时文件不备份系统空间不足时可能清理
Temp短期临时文件不备份系统重启或定期清理
Application Support应用配置、数据库iTunes 备份不会自动清理

问题 3: SwiftData 的 @ModelActor 模式解决了什么问题?如何正确使用?

点击查看答案

解决的问题:

  • SwiftData 模型对象不是 Sendable,无法直接跨 Actor 传递
  • 主线程数据库操作会阻塞 UI

正确使用方法:

  1. 定义 @ModelActor actor 类
  2. 在 actor 内使用 modelContext 执行操作
  3. 返回 PersistentIdentifier 而不是模型对象
  4. 在调用端通过 ID 加载模型
@ModelActor
actor DataImporter {
    func importUsers(from data: Data) throws -> [PersistentIdentifier] {
        let users = try JSONDecoder().decode([User].self, from: data)
        for user in users {
            modelContext.insert(user)
        }
        try modelContext.save()
        return users.map { $0.id }
    }
}

Phase 2 问题

问题 4: 为什么不能在 EventLoop 线程调用 Thread.sleep?正确的替代方法是什么?

点击查看答案

原因:

  • EventLoop 是单线程,管理数千连接
  • Thread.sleep 阻塞整个线程,所有连接的处理都会卡住
  • 这违反了 SwiftNIO 的非阻塞设计原则

正确替代:

  • 使用 eventLoop.scheduleTask(in: .seconds(1)) { }
  • 使用 Swift Concurrency 的 Task.sleep(nanoseconds:)
  • 使用 continuation 桥接 async 函数

问题 5: NIOLoopBoundBox 的作用是什么?如何使用?

点击查看答案

作用:

  • 包装 EventLoop-bound 值(如 Channel),使其符合 Sendable
  • 保证跨 Actor 访问时在正确的 EventLoop 上操作

使用方法:

let box = NIOLoopBoundBox(channel, eventLoop: eventLoop)

// 跨 Actor 使用
try await box.withValue { channel in
    // 这里在 channel 的 EventLoop 上执行
    channel.writeAndFlush(data, promise: nil)
}

问题 6: Process 的 run() 和 waitUntilExit() 有什么区别?

点击查看答案
方法作用阻塞性
run()启动子进程非阻塞,立即返回
waitUntilExit()等待子进程完成阻塞,直到进程退出
terminate()发送 SIGTERM非阻塞,请求终止

关键点:

  • run() 后进程独立运行,父进程继续执行
  • waitUntilExit() 阻塞父进程,等待子进程
  • 正确顺序:run() → (可选操作) → waitUntilExit()

问题 7: XCTestCase 的 setUp 和 tearDown 在什么时候执行?

点击查看答案

执行时机:

  • setUp: 在每个测试方法执行前调用
  • tearDown: 在每个测试方法执行后调用
  • 执行顺序: setUp → testMethod → tearDown → setUp → nextTestMethod → tearDown

关键点:

  • 每个测试前后都执行,保证测试隔离
  • setUp 用于初始化共享资源
  • tearDown 用于清理资源,防止测试间影响

Phase 3 问题

问题 8: Vapor 的路由和中间件是如何工作的?

点击查看答案

路由:

  • app.get("api", "hello") { req in ... } 将 URL 路径映射到处理函数
  • 支持 GET/POST/PUT/DELETE 等 HTTP 方法
  • 路径参数通过 req.parameters.get("id") 提取

中间件:

  • 实现 AsyncMiddleware 协议,respond(to:chainingTo:) 方法
  • 在请求到达路由前或响应返回前拦截
  • 使用 app.grouped(middleware) 创建中间件组

问题 9: GRDB 的 FetchableRecord 和 PersistableRecord 有什么区别?

点击查看答案
协议作用方法
FetchableRecord从数据库行解码为 Swift 类型init(row:)
PersistableRecord将 Swift 类型写入数据库encode(to:), insert(), update()

通常同时遵循两个协议实现完整的 CRUD 能力。


问题 10: Actor 和 Class 的主要区别是什么?

点击查看答案
特性ActorClass
并发安全✅ 编译器保证❌ 需要手动同步
访问方式需要 await直接访问
引用语义值语义(隔离)引用语义
继承不支持支持
适用场景共享可变状态需要继承或引用语义

问题 11: some 和 any 在什么场景下使用?

点击查看答案
  • some Protocol: 返回单一具体类型,隐藏实现细节,零性能开销。适用于 API 返回值、SwiftUI body
  • any Protocol: 存储异构集合,运行时类型擦除,有动态分发开销。适用于 [any Shape] 数组。

问题 12: Property Wrapper 的 wrappedValue 和 projectedValue 有什么区别?

点击查看答案
  • wrappedValue: 属性的实际值,通过 property 访问
  • projectedValue: 包装器暴露的额外接口,通过 $property 访问
  • 典型例子:SwiftUI 的 @State var countcount 是值,$count 是 Binding

📈 自我评估

完成以下检查项,评估你的掌握程度:

Phase 1 掌握度

  • 能够使用 JSONDecoder 解析嵌套 JSON 并处理可选字段
  • 能够使用 CodingKeys 自定义 JSON 键名映射
  • 能够正确获取 Documents/Caches/Temp 目录路径
  • 能够创建临时文件并理解 RAII 自动清理机制
  • 能够使用 AsyncLineSequence 流式读取大文件
  • 能够定义 @Model 数据类并提供初始化器
  • 能够配置 ModelContainer 并执行 CRUD 操作
  • 能够使用 #Predicate 和 FetchDescriptor 查询数据
  • 能够使用 @ModelActor 在后台线程安全操作数据库
  • 能够使用 ProcessInfo 读取系统环境变量
  • 能够使用 swift-dotenv 加载 .env 文件

Phase 2 掌握度

  • 理解 EventLoop 如何在单线程管理多连接
  • 能够创建 SwiftNIO Echo Server
  • 能够使用 ByteBuffer 读写数据并理解零拷贝
  • 能够将 EventLoopFuture 桥接到 async/await
  • 理解 NIOLoopBoundBox 解决的问题
  • 能够使用 Process 执行命令并捕获输出
  • 理解 Signal 处理的注意事项
  • 知道 macOS 和 Linux 的路径差异
  • 能够编写 XCTestCase 测试类
  • 能够使用 async 测试方法
  • 能够使用 measure {} 性能测试

Phase 3 掌握度

  • 能够使用 Vapor 创建 REST API 和路由
  • 理解 Vapor 中间件的工作原理
  • 能够使用 GRDB 定义 Record 并执行 CRUD
  • 理解 QueryInterface 的类型安全查询
  • 能够定义 Actor 并理解隔离域
  • 理解 Sendable 协议和编译器并发检查
  • 能够定义自定义 Property Wrapper
  • 理解 wrappedValue 和 projectedValue 的区别
  • 能够使用 weak/unowned 打破 ARC 循环引用
  • 理解 some vs any 的选择策略
  • 能够使用 withUnsafeBufferPointer 安全访问指针
  • 理解 MemoryLayout 的 size/stride/alignment
  • 理解 Swift Macros 的编译时代码生成机制
  • 能够定义自定义 Result Builder
  • 理解 SwiftUI ViewBuilder 的工作原理
  • 能够使用 Mirror 运行时检查类型结构

➡️ 下一步

完成高级进阶复习后,继续学习 实战精选 部分,你将:

  • 学习第三方库集成
  • 实现 LeetCode 题目
  • 掌握工程化最佳实践

返回: 高级进阶概览