关于 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 学习之旅!

Getting Started

hello-swift

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 编写单元测试、异步测试、性能基准

📚 章节列表

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 分钟

平台要求:

  • SwiftData: macOS 14.0+ (Sonoma)
  • SwiftNIO: macOS 12.0+ 或 Linux (Ubuntu 22.04+)
  • 文件操作 async APIs: macOS 12.0+

🔗 前置要求

必须完成:

  • 基础部分所有章节(变量与表达式 → 并发编程)
  • 理解 Swift 的 async/await 语法
  • 理解 do/catch/try 错误处理模式

建议具备:

  • 基本的文件系统和命令行操作经验
  • 了解 JSON 数据结构
  • 了解 TCP/IP 网络基础概念

📈 学习路径

Phase 1 (数据处理):
JSON 处理 → 文件操作 → SwiftData 持久化 → 环境配置

Phase 2 (网络与系统):
SwiftNIO 网络基础 → SwiftNIO async/await → 系统编程 → 测试框架

✅ 学习检查点

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

  • 使用 JSONDecoder 和 SwiftyJSON 解析嵌套 JSON 数据
  • 使用 FileManager 创建、读取、删除文件和目录
  • 使用 SwiftData @Model 定义数据模型并执行 CRUD 操作
  • 使用 swift-dotenv 管理 .env 环境变量
  • 使用 SwiftNIO 创建简单的 Echo Server
  • 将 SwiftNIO Future 与 async/await 桥接
  • 使用 Process 执行外部命令并捕获输出
  • 使用 XCTest 编写单元测试和异步测试

🎓 实践项目

建议练习:

  1. 编写一个读取 JSON API 响应并保存到 SwiftData 数据库的应用
  2. 实现一个从 .env 加载配置并写入日志文件的工具
  3. 创建一个 SwiftNIO Echo Server,支持多客户端并发连接
  4. 为你的代码库编写 XCTest 测试覆盖核心逻辑
  5. 实现一个 CLI 工具,调用 git 命令获取仓库信息

➡️ 下一步

完成高级进阶后,继续学习 实战精选 部分,你将看到:

  • 第三方库集成示例
  • 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 的约定,便于框架自动发现测试

继续学习

下一章: 阶段复习:高级部分 - 综合测试高级部分知识

返回: 高级进阶概览

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

返回: 高级进阶

阶段复习:高级进阶

完成高级进阶部分的八个章节后,让我们通过本复习章节巩固所学知识。


📋 知识回顾

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 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 {} 性能测试

➡️ 下一步

完成高级进阶复习后,继续学习 实战精选 部分,你将:

  • 学习第三方库集成
  • 实现 LeetCode 题目
  • 掌握工程化最佳实践

返回: 高级进阶概览