关于 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。
如何使用本书?
- 按顺序阅读:章节之间有依赖关系,建议从变量与表达式开始,依次学习。
- 动手实践:每章都有代码示例和练习,请务必在本地运行和修改代码。
- 对比学习:如果你熟悉其他语言,重点阅读 Swift 与 Rust/Python 对比表。
- 自我检测:每章末尾有知识检查题,检验你的理解程度。
- 回顾总结:完成基础部分后,阅读阶段复习巩固知识。
准备好了吗?让我们从快速开始开始你的 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 对比
| 特性 | Swift | Rust |
|---|---|---|
| 内存管理 | ARC (自动引用计数) | 所有权系统 (Ownership) |
| 空值处理 | Optional (? / if let) | Option<T> |
| 默认可变性 | let 不可变,var 可变 | let 不可变,let mut 可变 |
| 类型系统 | 值类型 + 引用类型 | 只有值类型 (结构体) |
| 抽象方式 | 协议 (Protocol) + POP | Trait + 泛型 |
| 并发模型 | async/await + Actor | async/await + Future |
| 错误处理 | throws / do-catch-try | Result<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 的核心概念和编程范式。这些知识是后续高级主题和实战应用的基石。
🎯 你将学到什么
完成本部分学习后,你将能够:
- 理解 Swift 类型系统 - 结构体、类、枚举、协议的核心区别
- 掌握值类型与引用类型 - Swift 的默认值语义
- 使用协议面向编程 (POP) - Swift 最独特的设计理念
- 编写泛型代码 - 可复用的类型安全代码
- 理解错误处理 - 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 的所有权系统 |
✅ 学习检查点
完成本部分后,你应该能够:
-
使用
let和var正确声明变量 - 使用可选类型安全地处理空值
- 使用枚举+关联值建模多状态数据
- 在结构体和类之间做出正确选择
- 定义协议并使用扩展提供默认实现
- 编写泛型函数和泛型类型
- 使用 do/catch/try 处理错误
- 使用闭包编写函数式代码
- 使用 async/await 编写并发代码
- 理解 Sendable 和 Actor 隔离
🎓 实践项目
建议练习:
- 创建一个简单的命令行计算器(变量、函数、控制流)
- 实现一个联系人管理工具(结构体、类、集合类型)
- 编写一个异步网络数据获取程序(并发编程、错误处理)
➡️ 下一步
完成基础部分后,继续学习 高级进阶 部分,你将学习:
- JSON 处理(JSONSerialization、Codable、SwiftyJSON)
- 文件操作(FileManager、临时文件)
- 系统服务(网络可达性、系统配置)
- 异步编程(Task、SwiftNIO 集成)
- 环境配置(swift-dotenv)
准备好了吗?让我们开始 变量与表达式 的学习! 🚀
变量与表达式
开篇故事
想象你有一个工具箱,里面装着各种工具:螺丝刀、锤子、尺子。你给每个工具贴上标签,下一次需要时就知道去哪里找。Swift 中的变量就像这些贴标签的工具箱 - 它们帮你存储和管理程序中的数据。表达式则是你使用这些工具完成的工作。
在 Swift 中,变量声明有一个非常特别的设计:默认情况下,所有变量都是不可变的。这就像你写在纸上的数字 - 写完后就不能改变。如果你想改,需要重新写一张纸。这个设计让代码更安全、更容易理解。
本章适合谁
如果你是 Swift 初学者,想理解如何存储数据、进行计算和控制程序流程,本章适合你。这是所有编程的基础,即使你是第一次接触编程也能理解。
你会学到什么
完成本章后,你可以:
- 使用
let关键字声明不可变变量 - 使用
var关键字声明可变变量 - 理解 Swift 类型推断机制
- 使用字符串插值构建动态文本
- 区分常量 (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 要这样设计?
- 安全性:防止意外的数据修改
- 并发安全:不可变数据可以安全地在线程间共享
- 编译器优化:不可变值让编译器能做更多优化
类比:
就像你写在纸上的数字 - 写完后就不能改变。如果你想改,需要拿一张新纸重写。
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
何时需要显式类型?
- 编译器无法推断时
- 你想覆盖默认推断(如
Int→Double) - 提高代码可读性
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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 不可变声明 | 无(约定用大写) | let x = 5 | let x = 5 | Swift/Rust 语法一致 |
| 可变声明 | x = 5 (总是可变) | let mut x = 5 | var x = 5 | Swift 用 var,Rust 用 mut |
| 类型注解 | x: int = 5 | let x: i32 = 5 | let x: Int = 5 | Swift 首字母大写 |
| 字符串插值 | f"{x}" | format!("{x}") | "\(x)" | Swift 用 \(x) |
| 常量关键字 | 无 | const (编译时) | let (运行时) | Swift let 是运行时常量 |
动手练习
练习 1: 预测输出
不运行代码,预测下面代码的输出:
let x = 5
let y = x + 3
print("y = \(y)")
点击查看答案
输出:
y = 8
解析:
x = 5- 声明常量 xy = x + 3- x + 3 = 8- 字符串插值输出
练习 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 借鉴了函数式编程的理念:
- 安全性:防止意外的数据修改
- 并发安全:不可变数据可以在线程间安全共享
- 编译器优化:不可变值让编译器能做更多优化
小结
核心要点:
let声明不可变常量 - 这是 Swift 的默认设置var声明可变变量 - 只在需要修改时使用- Swift 自动推断类型 -
let x = 5推断为Int - 字符串插值用
\(变量)- 在字符串中嵌入表达式 - 变量遮蔽允许复用名称 - 在不同作用域可以重新声明
关键术语:
- 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 推断为 Double,Double(radius) 将 Int 转为 Double,Double * Double = Double。
延伸阅读
学习完变量与表达式后,你可能还想了解:
- Swift 官方文档 - The Basics - 基础语法深入
- Swift by Sundell - Let vs Var - 最佳实践
选择建议:
💡 记住:不可变性是 Swift 的默认设置 - 如果你不特别告诉它"这个要改变",Swift 会让它保持不变。这是为了你的安全!
继续学习
基础数据类型
开篇故事
去超市购物时,你会用不同的容器装东西:塑料袋装水果,玻璃罐装酱料,纸盒装鸡蛋。每种容器装不同类型、不同数量的东西。Swift 的数据类型也是这个道理。
Int 和 Double 像标了容量的瓶子,String 像可以无限拉长的绳子,Array 是排队的一列物品,Set 是一袋不重复的糖果,Dictionary 是你手机通讯录里的名字和号码对应表。理解了这些容器,你就能在程序中存储和操作任何数据。
本章适合谁
你已经学完变量与表达式,知道 let 和 var 的区别。这一章带你深入 Swift 的类型系统。如果你从未接触过强类型语言,或者好奇 Swift 和 Python/Rust 在类型方面的差异,这一章很适合你。
你会学到什么
完成本章后,你可以:
- 区分和使用 Swift 的数值类型(Int、Float、Double)和布尔类型(Bool)
- 创建和操作 String、Array、Set、Dictionary 四种集合类型
- 使用元组 (Tuple) 组合多个返回值
- 理解类型推断与显式类型标注的区别和适用场景
- 使用可选类型 (Optional) 安全地处理缺失值
前置要求
完成 变量与表达式 的学习。本章会大量使用 let、var 和字符串插值。
第一个例子
打开 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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 整数类型 | int (任意精度) | i32, i64 等 | Int, Int8, Int32 | Swift 的 Int 是平台自适应位宽 |
| 浮点类型 | float (64位) | f32, f64 | Float, Double | Swift 默认推 Double |
| 布尔类型 | True/False | true/false | true/false | Python 首字母大写 |
| 字符串 | "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 元组语法几乎相同 |
| 可选类型 | 用 None | Option<T> | T? | Swift 用 ? 后缀最简洁 |
| 类型推断 | 动态类型 | let x = 5 | let x = 5 | Python 完全动态;Rust/Swift 静态推断 |
动手练习
练习 1: 类型判断
不运行代码,判断下面每个变量的类型:
let a = 42
let b = 3.14
let c = [1, 2, 3]
let d: Float = 2.5
let e = "hello"
点击查看答案
a:Intb:Doublec:[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) 中的转换是必须的,因为 Int 和 Int 相除还是 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: Int 和 Int32 有什么区别?什么时候该用哪个?
A: Int 是当前平台的自然位宽,在 64 位机器上是 64 位,在 32 位机器上是 32 位。Int32 固定 32 位。
- 日常编程用
Int:这是 Swift 的默认选择,性能最好 - 和 C/外部库交互时用固定位宽类型:如
Int32、UInt8,确保二进制布局匹配 - 处理网络协议或文件格式时用固定位宽类型:确保跨平台一致性
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: 元组是临时的轻量组合,适合短场景返回值(如函数返回多个值)。结构体是正式类型,定义了字段名和方法,适合在代码中反复使用。
- 临时分组用 元组
- 需要复用、传递、扩展用 结构体
小结
核心要点:
- 数值类型有明确位宽 —
Int、Int8、Float、Double,不同类型不能直接运算 - String 是值类型 — 支持拼接 (
+) 和插值 (\(...)) - Array 有序可重复,Set 无序不重复,Dictionary 键值映射 — 三种各有适用场景
- 元组组合多值 — 命名元组比
tuple.0可读性好得多 - 可选类型
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)
c 和 d 的值和类型分别是什么?
答案与解析
答案: c = 8.0(Double),d = 8(Int)
解析:
Double(a)把5转为5.0,5.0 + 3.0 = 8.0(Double)Int(b)把3.0转为3,5 + 3 = 8(Int)- 关键是看运算中最高精度的类型来决定结果类型。
延伸阅读
学完基础数据类型后,你可能还想了解:
- Swift 官方文档 - Basic Operators - 运算符深入
- Swift 官方文档 - Collection Types - 集合类型完整参考
选择建议:
💡 记住:Swift 是强类型语言。编译器比你更早知道类型错误,善用类型推断,但不要害怕显式标注。
继续学习
控制流
开篇故事
你每天早上醒来,大脑就在运行控制流。如果室外温度低于 10 度,你穿外套。如果今天是星期一到星期五,你去上班。否则,你休息。你会反复做同一件事直到满足某个条件,比如刷牙刷了两分钟才停下。
程序也是如此。代码不一定从上到下逐行执行。有时你要根据条件走不同的分支,有时你要反复做一件事直到某个条件满足,有时你要提前结束一段逻辑。Swift 提供了丰富的控制流工具来描述所有这些场景。
本章适合谁
你已经学完变量与数据类型,理解了 let、var 和各种集合类型。这一章带你学会让程序做出决策和重复执行任务。如果你写过任何语言的条件判断或循环,你会在这里看到 Swift 的独到设计。
你会学到什么
完成本章后,你可以:
- 使用
if/else进行条件分支判断 - 使用
switch/case处理多路分支,包括范围匹配和元组匹配 - 使用
for-in遍历数组、字典和各种范围 - 使用
while和repeat-while编写循环 - 使用
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是一个布尔表达式,结果为true或false- 三个分支互斥,只会执行其中一个
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 through:stride(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 支持给循环打标签,这样可以在嵌套循环中精确控制 break 和 continue 跳出的层级:
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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| if/else | if x > 0: | if x > 0 { } | if x > 0 { } | Python 用缩进;Rust/Swift 用大括号 |
| 条件类型 | 任意 Truthy 值 | bool | Bool | Swift 严格要求 Bool,不接受整数 |
| switch | 无(用 if/elif) | match | switch | Python 3.10 有 match,但能力有限 |
| break 关键字 | 需要 | 不需要 | 不需要 | Swift/Rust 的 case 不穿透 |
| 范围遍历 | range(1, 4) | 1..=3 或 1..3 | 1...3 或 1..<3 | 三种语言范围语法各不相同 |
| 遍历数组 | for x in lst: | for x in &vec | for x in array | 语法几乎一致 |
| 带索引遍历 | enumerate(lst) | iter.enumerate() | array.enumerated() | Swift 返回 (index, element) 元组 |
| while 循环 | while cond: | while cond { } | while cond { } | 语义完全一致 |
| do-while | 无 | loop { } | repeat { } while | Python 没有 do-while |
| guard / 早期退出 | 无 | ? 和 if let | guard | Swift 独有 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)。
小结
核心要点:
- if/else 做条件分支 — 条件必须是 Bool,大括号不可省
- switch 自动 break,必须穷尽 — 支持范围、元组等模式匹配
- for-in 遍历数组、字典、范围 —
enumerate()带索引 - while 先检查,repeat-while 先执行 — 后者至少执行一次
- 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 == 0且2 != 8,进入 if 分支。打印8 is not prime,然后break searchLoop跳出整个外层循环 - 最终结果:
7 is prime,然后8 is not prime,循环结束
延伸阅读
学完控制流后,你可能还想了解:
- Swift 官方文档 - Control Flow - 控制流完整参考
- Swift 官方文档 - Pattern Matching - 模式匹配深入
选择建议:
💡 记住:Swift 的控制流设计哲学是"让正确的写法自然,让错误的写法不可能"。强制 Bool 条件、必须穷尽的 switch、自动 break 的 case —— 这些设计减少了你能犯的错误。
继续学习
函数
开篇故事
想象你在一家餐厅工作。顾客点菜后,厨房里的厨师按照固定配方烹饪,然后把完整的菜品端上桌。每个配方就是一个函数:它接收食材(输入),经过一系列步骤加工,最后产出菜品(输出)。
在编程中,函数让你把重复的逻辑封装成可复用的"配方"。当你需要同一道菜时,不需要重新发明配方,只需要调用它即可。Swift 的函数设计兼具灵活性和安全性,让你可以自由组合参数与返回值。
本章适合谁
如果你希望学会如何组织代码、减少重复、写出可读性强的程序,本章适合你。无论你是第一次写函数,还是从其他语言转来学习 Swift 的函数特性,本章都会帮你快速上手。
你会学到什么
完成本章后,你可以:
- 使用
func关键字定义和调用函数 - 理解外部参数名(external name)和省略符
_的使用 - 为参数设置默认值,声明可变参数列表
- 使用
inout参数和元组实现多返回值 - 把函数当作值传递,包括嵌套函数和闭包返回
前置要求
本章前置知识:阅读过 变量与表达式,熟悉 let、var 和基本类型。
第一个例子
打开 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.min 和 bounds.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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 函数定义 | def name(x): | fn name(x: Type) -> Ret | func name(x: Type) -> Ret | Swift/Rust 返回类型用箭头 |
| 参数标签 | 无,位置参数 | 无 | func name(label param: Type) | Swift 默认有外部标签 |
| 省略标签 | *args | N/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 | 闭包 ` | x | x` |
动手练习
练习 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: 可以,使用元组。元组不仅返回多个值,还可以给每个元素命名,调用后用 .元素名 访问,非常直观。
小结
核心要点:
func定义函数 — 参数带外部标签(或用_省略),-> Type指定返回类型- 默认参数值 — 用
= defaultValue提供可选参数 - 可变参数
...— 接受任意数量同类型参数,内部以数组形式访问 inout参数 — 用&传入var变量,在函数内可以修改原始值- 元组返回多值 —
(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 中参数标签是 to 和 port,调用时必须使用对应标签。注意参数名称和本地标签可以不同。
问题 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 官方文档 — Functions — 函数定义完整语法
- Swift by Sundell — Closures — 闭包深度解析
选择建议:
记住:Swift 的函数设计强调可读性。参数标签让你的调用像自然语言一样清晰,这是一项值得坚持的编码习惯。
继续学习
枚举
开篇故事
想象你是一个气象站的预报员。天气有几种固定的状态:晴天、雨天、雪天、多云。但你不能简单用一个字符串表示,因为类型拼写错误会让程序崩溃。
Swift 的枚举(enum)就是为这种场景而生的。它限制值只能从一组预定义的选项中选择,让编译器帮你检查所有可能性。不仅如此,Swift 的枚举比其他语言更强大:它可以携带额外数据,可以定义行为方法,甚至可以递归定义数据结构。
本章适合谁
如果你希望用类型安全的方式管理一组相关的选择,或者想理解 Swift 枚举为什么被称为"最像数据结构的枚举",本章适合你。从简单的状态标识到复杂的模式匹配,枚举是 Swift 编程中最重要的基础之一。
你会学到什么
完成本章后,你可以:
- 使用
enum关键字定义基本枚举类型 - 理解关联值(associated values)和原始值(raw values)的区别
- 使用
CaseIterable协议和allCases遍历所有枚举值 - 用
switch语句对枚举进行穷尽匹配 - 使用
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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 基本枚举 | 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 enum | Optional[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 类似。
小结
核心要点:
enum定义枚举类型 — 成员通过case声明,彼此独立- 关联值 — 让每个成员携带不同的附加数据
- 原始值 — 让枚举有基础类型(String/Int),支持
rawValue读写 CaseIterable— 自动生成allCases列表,方便遍历- 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,外层 mul 把 add 的结果乘以 2。这就是抽象语法树(AST)的典型表示方式。
延伸阅读
学习完枚举后,你可能还想了解:
- Swift 官方文档 — Enumerations — 枚举完整语法
- Swift by Sundell — Enums — 枚举在实战中的最佳实践
选择建议:
记住:Swift 的枚举是真正的数据类型,不是简单的整数别名。它可以携带数据、定义方法、实现协议——这是 Swift 类型安全的基石。
继续学习
结构体
开篇故事
想象你正在整理名片柜。每张名片记录一个人的信息:姓名、电话、邮箱。你把这张名片复印一份给朋友,朋友在上面改了电话号码。那张复印名片会更新,你原始的名片会跟着变吗?
不会。因为名片是复印件,修改复印件不会影响原件。这就是 Swift 中结构体(struct)的核心行为——值类型(value type)的语义:赋值和传递时,数据会被完整复制,彼此独立。理解这一点,你就掌握了 Swift 内存模型的一半。
本章适合谁
如果你希望学会如何自定义数据容器,管理一组相关字段,并理解 Swift 中值类型与引用类型的区别,本章适合你。结构体是 Swift 编程中最常用的自定义类型。
你会学到什么
完成本章后,你可以:
- 使用
struct关键字定义自定义数据结构 - 区分存储属性(stored properties)和计算属性(computed properties)
- 使用属性观察器(
willSet/didSet)监听属性变化 - 理解值类型(value type)的"按值复制"语义
- 掌握
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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 结构体定义 | class Point: x, y | struct 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 self | mutating func | Rust 用 borrow checker 静态保证 |
| 自动初始化器 | dataclass(装饰器) | 手动或宏 derive | 自动逐成员初始化器 | Swift 编译器自动生成 |
| 静态成员 | @staticmethod + @classmethod | impl 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 标准库的类型(Int、String、Array 等)全部是结构体,这是有意的设计抉择。只有在需要继承或多态行为时才考虑 class。
Q: mutating func 和类的方法有什么不同?
A: 结构体的 self 默认只读,所以修改属性的方法需要 mutating。类的 self 本来就是可变的(引用类型),不需要修饰符。
Q: 属性观察器 willSet 和 didSet 有什么区别?
A: willSet 在赋值之前触发,newValue 是即将设定的值。didSet 在赋值之后触发,oldValue 是旧的值。通常用 didSet 来响应变化做后续操作(比如刷新 UI)。
小结
核心要点:
struct定义值类型 — 赋值时复制数据,彼此独立- 存储属性存数据 —
let不可变、var可变 - 计算属性通过 getter 算出 — 不存储,不占用内存
- 属性观察器监听变化 —
willSet/didSet在修改前后触发 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.swift(Matrix 结构体部分)
知识检查
问题 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 是值复制,t2 和 t1 完全独立。修改 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 官方文档 — Structures and Classes — 结构体与类的完整对比
- Swift by Sundell — Structs vs Classes — 何时选择哪种类型
选择建议:
记住:Swift 中一切以值类型为首选。结构体、枚举让你写出无副作用的代码。类是最后的手段,只有在继承和多态真正需要时才使用。
继续学习
类与对象
开篇故事
想象你有一座积木工厂。你设计了一个基础积木模板,上面有凸起的点和连接槽。然后你在这个模板基础上,做出了不同形状的积木:带轮子的、带窗户的、带门的。它们都继承了基础积木的连接方式,但各自有不同的功能。
Swift 中的类(Class)就跟这个模板工厂一样。你定义一个基础类,然后让其他类继承它的特性,再添加自己的独有功能。
在 Swift 中,类是引用类型(Reference Type)。这意味着当你把类实例赋值给另一个变量时,你传递的是指向同一个实例的"遥控器",而不是拷贝整个实例。这跟结构体(Struct)的值类型语义完全不同。
本章适合谁
如果你已经理解了变量、数据类型和函数,想深入学习 Swift 的面向对象编程,本章适合你。无论你是从 Python、Java 还是 Rust 过来,本章会帮你理解 Swift 类的独特设计。
你会学到什么
完成本章后,你可以:
- 使用
class关键字定义类并使用继承 - 理解指定初始化器(Designated Initializer)和便捷初始化器(Convenience Initializer)
- 使用
deinit管理资源清理和 ARC 内存管理 - 掌握引用类型与值类型的区别以及身份运算符
===和!== - 使用
is、as?、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) // 使用指定初始化器
初始化规则:
- 指定初始化器必须初始化本类声明的所有属性,然后调用父类的初始化器
- 便捷初始化器必须委托给同一个类的另一个初始化器
- 子类不会自动继承父类的初始化器,除非满足特定条件
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 使用 is、as?、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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 类定义 | class Foo: | 无(用 struct + impl) | class Foo { } | Rust 没有类,只有结构体 |
| 继承 | 支持多继承 | 无(用 Trait) | 单继承 | Swift/Rust 都倾向组合优于继承 |
| 引用计数 | 自动(CPython 内部) | 手动(Rc/Arc) | 自动(ARC) | Swift ARC 编译期插入 retain/release |
| 可变性 | 默认可变 | 默认不可变(let/mut) | 引用可变(属性可标 var) | Python 类实例默认可变 |
| 析构函数 | __del__ | Drop trait | deinit | Swift deinit 不接受参数 |
| 类型转换 | isinstance() | dyn Trait + downcast | is, as?, as! | Swift 提供安全的 Optional 转换 |
| 身份比较 | is | Rc::ptr_eq | ===, !== | Python/Rust 都有对应方案 |
动手练习
练习 1: 创建类层次结构
设计一个动物类层次结构:
- 基类
Animal,包含名称和发出声音的方法 - 子类
Dog和Cat,各自实现不同的叫法 - 创建一个
[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: 类型转换统计
给定一个混合数组包含 Movie、Song 和 MediaItem,使用类型转换统计每种类型的数量:
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: 修复引用循环
下面的代码会导致内存泄漏,请用 weak 或 unowned 修复:
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: weak 和 unowned 到底怎么选?
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/协议是更干净的设计
小结
核心要点:
- 类是引用类型 - 赋值和传参时共享同一个实例,而不是拷贝
- 继承用冒号 -
class Child: Parent,子类用override重写父类成员 - ARC 自动管理内存 - 引用计数归零时自动释放,
deinit用于清理资源 weak和unowned打破强引用循环 - 避免内存泄漏- 类型转换用
is和as?- 安全地检查并转换类型,避免使用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
解析: c1 和 c2 指向同一个 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") }
}
延伸阅读
学习完类与对象后,你可能还想了解:
- Swift 官方文档 - Classes and Structures - 类与结构体的完整说明
- Swift 官方文档 - Inheritance - 继承深入
- Swift Blog - ARC - ARC 内存管理原理
选择建议:
💡 记住:Swift 默认推荐结构体,类是在需要引用语义时才选择的高级工具。不要滥用继承,优先考虑协议和组合。
继续学习
- 下一步:协议 - 理解 Swift 的 Protocol-Oriented Programming 范式
- 相关:泛型 - 编写类型安全的通用代码
- 进阶:错误处理 - do/catch/try 错误传递机制
协议
开篇故事
想象你开了一家餐厅。你不需要知道每个厨师来自哪里,受过什么训练,甚至不需要他们是正式员工还是临时工。你只需要确保:每个厨师都能按照菜谱做菜,都能在规定时间完成出餐,都能保证菜品卫生。
Swift 中的协议(Protocol)就是这份"菜谱"。它定义了一组要求,任何类型只要满足这些要求,就能被接受。结构体可以遵守协议,类可以遵守协议,枚举甚至可以扩展已有类型来遵守协议。
协议是 Swift 区别于其他语言的核心特性之一。Swift 倡导面向协议编程(Protocol-Oriented Programming, POP),而不是传统的类继承体系。这种设计让代码更灵活、更可组合、更容易测试。
本章适合谁
如果你理解了类、继承和值类型的概念,想理解 Swift 更灵活的抽象方式,本章适合你。如果你来自 Java 或 C#,你会发现 Swift 的协议比接口更强大。如果你来自 Rust,你会发现 Swift 协议和 Trait 有很多相似之处,但也有自己的特点。
你会学到什么
完成本章后,你可以:
- 使用
protocol关键字定义协议并让类型遵守 - 理解协议方法、属性要求和
mutating关键字 - 使用协议扩展(Protocol Extension)提供默认实现
- 掌握协议组合(Protocol Composition)和
some关键字 - 理解面向协议编程(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"
协议要求包括:
- 实例属性和类型属性(
var、static var) - 实例方法和类型方法(
func、static 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 循环能适用于 Array、Dictionary、String 等所有序列类型。
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 标准库几乎完全基于协议构建:Equatable、Comparable、Hashable、Sequence、Collection、Codable ...... 这些协议让你的自定义类型一键获得丰富的功能。
常见错误
错误 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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 协议/接口 | ABC(抽象基类)/ Protocol (typing) | Trait | Protocol | Swift 协议可以有默认实现 |
| 扩展已有类型 | 不能(需猴子补丁) | 可以(impl Trait for Type) | 可以(extension Type: Protocol) | Swift 和 Rust 都支持 |
| 多继承 | 支持 | 不支持(实现多个 Trait) | 不支持(遵守多个协议) | Swift 协议组合用 & |
| 关联类型 | 无 | Associated Type | associatedtype | Swift/Rust 高度相似 |
| 不透明返回 | 无 | impl Trait | some Protocol | 等价功能 |
| 分发方式 | 动态 | 静态(泛型)或动态(dyn) | 静态(泛型/some)或动态(协议) | Swift some 用静态分发 |
| mutating | self 总是可变 | &mut self | mutating(值类型) | 概念类似,语法不同 |
动手练习
练习 1: 协议的基本遵守
定义一个 Mathable 协议,要求有一个 calculate() → Double 方法。让 Rectangle 和 Circle 两个结构体遵守它,各自实现面积计算。
点击查看答案
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 类型,遵守以下协议协议定义了 push、pop 和 peek 方法:
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 Protocol 和 protocol 类型作为返回值有什么区别?
A: 关键区别在于类型确定性和性能:
some Protocol(不透明类型)— 编译器知道具体类型,可以做内联优化,但所有返回路径必须是同一具体类型protocol(协议类型)— 运行时动态分发,允许不同返回路径,但有一点性能开销
如果你每次返回相同的类型,用 some。如果你需要返回不同类型的值,用协议类型。
Q: associatedtype 和泛型参数有什么区别?
A: 它们解决的问题不同:
- 泛型参数 — 调用者决定。比如
Array<Int>,你告诉 Array 元素的类型 - 关联类型 — 实现者决定。比如你实现的
Container,你把Item设为什么就是什么,使用者不用管
类比思考:泛型参数像是菜单上点菜(客人选),关联类型像是套餐配什么菜(厨师定)。
小结
核心要点:
- 协议定义类型要求 - 属性、方法、下标可以成为协议要求
- 协议扩展提供默认实现 - 让遵守者可选覆盖,类似"继承"但不限制类型
- 协议作为类型使用 - 实现多态,支持
[Protocol]数组和协议参数 associatedtype定义占位类型 - 由遵守者决定具体类型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 返回 Box 和 Line 两个不同具体类型,违反了此约束。
修复 — 改用协议类型(动态分发):
func createDrawing(useBox: Bool) -> Drawable { // ✅ 协议类型
if useBox {
return Box()
} else {
return Line()
}
}
或者固定返回一种类型:
func createBox() -> some Drawable { // ✅ 总是返回 Box
Box()
}
延伸阅读
学习完协议后,你可能还想了解:
- Swift 官方文档 - Protocols - 协议完整说明
- WWDC 2015 - Protocol-Oriented Programming in Swift - Apple 官方 POP 讲座
- Swift by Sundell - Protocol Extensions - 协议扩展最佳实践
选择建议:
- 初学者 → 继续学习 泛型,理解类型安全的通用代码
- 想深入 Swift 设计哲学 → 阅读 Swift 标准库协议体系
💡 记住:Swift 的最佳实践是"协议优先"。能不用继承就不用继承。用协议定义能力,用扩展提供默认实现,用值类型保证安全。
继续学习
泛型
开篇故事
想象你有一个万能工具箱。这个箱子里有扳手、螺丝刀、钳子,但每个工具都能适配不同尺寸的螺母和螺丝。你不需要为每种尺寸买一套工具,一套就够了。
Swift 中的泛型(Generics)就是这个万能工具。你可以写一段代码,让它适用于多种类型,而不是为每种类型复制一份。当你使用 Array<Int>、Array<String> 或 Array<Double> 时,其实底层都是同一份 Array 实现,只是填入了不同的类型参数。
Swift 的标准库几乎完全建立在泛型之上。Array、Dictionary、Optional、Result 都是泛型类型。理解泛型不只是写更少的代码,更是理解 Swift 的类型系统如何做到既灵活又安全。
本章适合谁
如果你已经理解了 Swift 的类型系统和协议基础,想理解如何编写能复用于多种类型的代码,本章适合你。如果你从 Java、C++ 或 Rust 过来,你会发现 Swift 的泛型语法和它们有相似之处,但协议约束系统有自己的独特设计。
你会学到什么
完成本章后,你可以:
- 使用
<T>语法定义泛型函数和泛型类型 - 使用类型约束(Type Constraint)限制泛型参数必须符合特定协议
- 使用
where子句对泛型添加多重约束 - 理解 Swift 标准库中的泛型设计(Array、Dictionary、Optional、Result)
- 在泛型与协议之间做出正确的选择
前置要求
确保你已经阅读了 协议 一章,理解协议定义和协议约束的概念。本章的类型约束会大量用到 Equatable、Comparable、Hashable 等协议。
第一个例子
打开 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允许函数修改传入的参数- 同一个函数可以处理
Int、String或任何其他类型
输出:
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— 集合中的元素Key、Value— 字典的键值对- 单字母简短,描述性名称清晰,根据上下文选择
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 | <、>、<=、>= | 排序和比较 |
Hashable | hash(into:) | 作为字典键 |
CustomStringConvertible | description 属性 | 字符串表示 |
Codable | Encodable + Decodable | JSON 编解码 |
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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 泛型函数 | 无(运行时类型) | fn foo<T>(x: T) | func foo<T>(_ x: T) | 语法高度一致 |
| 类型约束 | 无(duck typing) | Trait bound T: Trait | Protocol constraint T: Protocol | Swift/Rust 约束在编译期 |
| where 子句 | 无 | where T: Trait | where T: Protocol | Swift/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 标准库),包含 success 和 failure 两个 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 的行为(也是单态化)不同。
小结
核心要点:
<T>是泛型类型参数 — 让函数和类型适用于多种类型- 类型约束
T: Protocol限制可选类型 — 最常用的约束是Equatable、Comparable where子句添加多重或关联类型约束 — 泛型的强大表达能力- 标准库大量使用泛型 —
Array、Dictionary、Optional、Result都是泛型 - 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 Clause | where 子句 |
| 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")
x 和 y 的类型分别是什么?
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 推断为 Int,wrap("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 官方文档 - Generics - 泛型完整说明
- Swift 官方文档 - Generic Parameters and Arguments - 泛型参数深入
- Swift by Sundell - Generics - 泛型最佳实践
选择建议:
- 初学者 → 继续学习 错误处理 或回顾协议知识
- 想深入 Swift 泛型体系 → 阅读 Swift 标准库源码(开源在 GitHub)
💡 记住:泛型是 Swift 类型安全的核心支柱。标准库的每一个集合类型、每一个错误处理机制都以泛型为基础。写泛型代码时,约束应该尽量宽松,满足实际需求即可。
继续学习
错误处理
开篇故事
想象你在一家餐厅点了一道菜。厨房收到订单后,开始准备。但在做菜的过程中,厨师发现食材用完了,或者火候不对。这时候他需要做两件事。
第一,他必须告诉服务员出了什么问题,让服务员转告客人。第二,如果他已经切了一些菜,必须把工作台清理干净。
Swift 的错误处理机制就是做这两件事的。它让函数能够告诉你"我遇到了一个无法处理的状况",同时确保资源被正确释放。
本章适合谁
如果你写过网络请求、文件读写,或者任何可能失败的操作,你的代码就需要错误处理。本章适合所有 Swift 开发者,无论你是刚开始学编程,还是已经从其他语言转过来了。
你会学到什么
完成本章后,你可以:
- 定义遵循 Error 协议 (Error protocol) 的自定义错误类型
- 使用 do-try-catch 模式 (do-try-catch pattern) 捕获和处理错误
- 理解 throws 关键字 (throws keyword) 的作用范围
- 区分try? (optional) 和 try! (force unwrap) 的使用场景
- 使用 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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 错误类型 | 继承 Exception | 实现 Error trait | 遵循 Error 协议 | Swift/Rust 相似 |
| 声明可能出错 | 无标记 | Result<T, E> 返回值 | throws 关键字 | Python 无编译时检查 |
| 尝试调用 | try/except | 手动匹配/? 操作符 | try + do/catch | 语法各有特色 |
| 直接抛出错误 | raise | Err(e)? / panic! | throw | Rust 没有 throw 关键字 |
| 清理资源 | finally | Drop trait (RAII) | defer 块 | 语义最接近的是 finally |
| 错误传递 | 异常自动上浮 | ? 操作符 | rethrows | Rust 更简洁 |
动手练习
练习 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— 函数一定可能抛错,调用者必须tryrethrows— 函数可能抛出闭包的错误。如果传入的闭包不抛错,调用者不需要try
标准库中的 map, flatMap 等全是 rethrows。
Q: Result 类型什么时候比 throws 更好?
A: Result 主要用在异步回调场景:
func fetchData(completion: (Result<Data, Error>) -> Void)
因为回调函数签名不能加 throws。对于普通的同步函数,直接用 throws + do-catch 更简洁。
小结
核心要点:
- 错误用枚举表示 — 遵循 Error 协议,case 可携带关联值
- throws 标记危险函数 — 调用时必须用 try
- do-catch 捕获错误 — 可以匹配特定错误类型,最后用通用 catch 兜底
- defer 在作用域退出时执行 — 清理资源,LIFO 顺序
- 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
解析: 回调函数签名无法用 throws。Result<Data, Error> 枚举是异步场景的标准做法,调用方可以区分成功和失败情况。
延伸阅读
学完错误处理后,你可能还想了解:
- Swift 官方文档 - Error Handling — 错误处理语言参考
- Swift 官方文档 - Defer Statement — defer 语句详解
选择建议:
记住:错误处理的核心是"让失败显式化"。Swift 不允许你忽略一个可能出错的函数调用。这是为了你的代码更安全!
继续学习
闭包
开篇故事
假设你在一个外卖平台上下了单。你填了地址、选了餐厅、付了款。但在等待的过程中,你并没有闲着。你继续工作、看书、聊天。等外卖到了,平台会通知你。
这就是闭包的精髓。你把一段代码"包好"交给系统,系统在合适的时候执行它。闭包可以记住它在创建时的环境,就像一个外卖订单记住了你的地址和菜品。
Swift 的闭包非常类似 JavaScript 的箭头函数或 Python 的 lambda,但它有更强的类型系统和作用域控制。
本章适合谁
本章适合已经掌握基本函数语法的 Swift 学习者。如果你能写出简单的函数定义,已经了解参数和返回值,就可以开始学闭包。如果你刚从其他语言转过来了,闭包是你一定会遇到的概念。
你会学到什么
完成本章后,你可以:
- 使用闭包表达式语法
{ (params) -> ReturnType in body } - 理解尾随闭包 (trailing closure) 语法糖
- 掌握闭包的值捕获 (value capturing) 机制
- 区分逃逸闭包 (@escaping) 和非逃逸闭包
- 使用高级函数 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
factor 在 makeMultiplier 返回后并没有消失。它被闭包捕获了,一直存在于内存中。
再看一个更生动的例子:
// 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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 匿名函数 | lambda | ` | args| { ... }` | { ... in ... } |
| 捕获 | 自动捕获 | 明确模式(move/borrow) | 自动强捕获 | Rust 需要 move 关键字 |
| 逃逸闭包 | 不需要(引用计数管理) | move | @escaping | Swift 需要标注逃逸闭包 |
| 尾随闭包 | 不支持(参数必须是最后一个) | 不支持 | 支持 | Swift 独有的语法糖 |
| 自动闭包 | 不支持 | 不支持 | @autoclosure | Swift 独有,用于断言等场景 |
| map/filter/reduce | map/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,可读性差。只用在一两行的简短闭包中。
小结
核心要点:
- 闭包是匿名函数 —
{ (params) -> ReturnType in body } - 尾随闭包是语法糖 — 最后一个闭包可以移到括号外
- 闭包可以捕获值 — 记住创建时的环境,类似于"携带状态的代码块"
- 逃逸闭包需标注 —
@escaping声明生命周期超出函数 - 高阶函数组合能力 — 用 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 变量。c1 和 c2 各持有独立的闭包,捕获了各自的环境。
问题 3 🟡 (逃逸闭包)
var handlers: [() -> Void] = []
func add(_ h: @escaping () -> Void) {
handlers.append(h)
}
为什么需要 @escaping?
答案与解析
答案: 因为闭包 h 被添加到了全局数组 handlers 中。它的生命周期超越了 add 函数的调用。
解析: 默认闭包是非逃逸的,只在函数体内有效。一旦闭包被保存到别处(变量、数组、线程),Swift 需要 @escaping 标记来知道这个闭包会存活更久,进而检查循环引用等问题。
延伸阅读
学完闭包后,你可能还想了解:
- Swift 官方文档 - Closures — 闭包语言参考
- Swift API Design Guidelines - Closures — 闭包命名的最佳实践
选择建议:
记住:闭包就是"带状态的代码块"。它能记住创建时的变量,在需要时用。掌握闭包后,你会发现 Swift 的 API 变得异常灵活。
继续学习
并发编程
开篇故事
想象你在一家餐厅的后厨工作。厨师长(主线程)负责摆盘和最终检查。但切菜、炒菜、洗碗这些活不能全让厨师长干。你需要帮手。
在 Swift 过去,我们用 Thread(线程)和 Semaphore(信号量)"人肉管理"这些帮手。就像厨师长站在厨房门口大喊"A去切菜,B去炒菜"。但这有个问题。如果两人都同时去拿同一把刀,就会打架。这叫做"数据竞争"。
Swift 的并发编程是现代化的厨房管理系统。你只需要说"帮我准备这道菜",系统自动分配人手,还能保证不会有人拿到同一把刀。
Sources/BasicSample/ConcurrencySample.swift 中有完整的并发示例。让我们从最基础的 async/await 开始。
本章适合谁
如果你写过网络请求、文件读写,或者任何需要等待外部资源的操作,你就能从并发编程中受益。本章适合有一定 Swift 基础的学习者。如果你是第一次接触 Swift,建议先完成 错误处理 和 闭包。
你会学到什么
完成本章后,你可以:
- 使用
async/await语法编写异步代码 - 理解 async let 实现并行绑定
- 使用
Task创建和管理后台任务 - 用
TaskGroup实现动态并行计算 - 掌握
actor和Sendable实现线程安全
前置要求
你需要先理解 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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 异步函数 | async def | async fn | async 函数 | 语法几乎一样 |
| 等待操作 | await | await | await | 完全一致 |
| 协程/任务 | asyncio.Task | tokio::Task | Task | 都类似 |
| 并行绑 | 不支持 | 直接 join TaskGroup` | async let | Python 需手动 |
| 共享数据 | GIL(锁) | Arc<Mutex<T>> | Actor + Sendable | Swift 编译时检查 |
| 数据竞争保护 | 无 | 所有权系统 + Sendable | Sendable + Strict Concurrency | Swift 6.0 编译时 |
| 异步流 | async for | Stream trait | AsyncSequence | 都有 |
动手练习
练习 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 类型会编译报错。
小结
核心要点:
- async/await 让异步代码可读性高 — 写起来像同步代码,但底层是异步的
- async let 实现并行 — 多个独立任务同时启动,最后 await
- Task 是并发基本单元 — 后台执行异步操作
- TaskGroup 管理大量任务 — 动态创建、自动等待
- 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 等待方法完成后再获取结果,确保数据一致性。
延伸阅读
学完并发编程后,你可能还想了解:
- Swift 官方文档 - Concurrency — 完整并发语言参考
- Swift 5.5 Release Notes — async/await 和 actor 首次引入
- Swift Evolution SE-304 Structured Concurrency — 结构化并发提案
- Swift 6.0 Strict Concurrency — 编译时数据竞争检查
选择建议:
- 初学者 → 继续学习 错误处理 或 闭包
- 有经验开发者 → 探索 进阶 JSON 处理 中的 SwiftNIO 异步编程
- 准备生产级应用 → 阅读 Swift 并发安全指南和 Sendable 要求
记住:Swift 的并发设计哲学是"让正确的做法变得简单"。async/await 让异步代码像同步代码一样易读,actor 让线程安全像类一样简单,Swift 6.0 让数据竞争在编译时就消失。这是现代并发编程的未来方向。
继续学习
高级进阶 (Advance)
📖 学习内容概览
欢迎完成 Swift 基础部分的学习!高级进阶 部分将带你深入 Swift 的生态系统与工程化实践。从 JSON 处理到异步编程,从系统服务到第三方库集成,这些知识将帮助你编写生产级别的 Swift 代码。
🎯 你将学到什么
完成本部分学习后,你将能够:
- 处理 JSON 数据 - 使用 JSONSerialization、JSONDecoder/Encoder 和 SwiftyJSON
- 操作文件系统 - 使用 FileManager 进行文件读写、目录遍历、临时文件管理
- 监控系统服务 - 网络可达性检测、系统配置信息查询
- 编写异步程序 - 使用 Task、async/await 与 SwiftNIO 进行异步 I/O
- 管理环境配置 - 使用 swift-dotenv 管理
.env环境变量
📚 章节列表
| 章节 | 说明 | 难度 | 预计时间 |
|---|---|---|---|
| JSON 处理 | JSONSerialization, JSONDecoder/Codable, SwiftyJSON | 🟡 中等 | 45 分钟 |
| 文件操作 | FileManager, 临时文件, AsyncLineSequence 流式读取 | 🟡 中等 | 40 分钟 |
| SwiftData 持久化 | @Model, ModelContainer, ModelActor, #Predicate | 🔴 困难 | 60 分钟 |
| 环境配置 | ProcessInfo, swift-dotenv, 动态成员查找 | 🟢 简单 | 30 分钟 |
注意: SwiftData 章节 requires macOS 14.0+。文件操作的流式读取特性 requires macOS 12.0+。
🔗 前置要求
必须完成:
- 基础部分所有章节(变量与表达式 → 并发编程)
- 理解 Swift 的
async/await语法 - 理解
do/catch/try错误处理模式
建议具备:
- 基本的文件系统和命令行操作经验
- 了解 JSON 数据结构
📈 学习路径
JSON 处理 → 文件操作 → SwiftData 持久化 → 环境配置
✅ 学习检查点
完成本部分后,你应该能够:
- 使用 JSONDecoder 和 SwiftyJSON 解析嵌套 JSON 数据
- 使用 FileManager 创建、读取、删除文件和目录
- 使用 SwiftData @Model 定义数据模型并执行 CRUD 操作
- 使用 swift-dotenv 管理 .env 环境变量
🎓 实践项目
建议练习:
- 编写一个读取 JSON API 响应并保存到 SwiftData 数据库的应用
- 实现一个从 .env 加载配置并写入日志文件的工具
- 创建一个使用 ModelActor 后台导入数据的模块
➡️ 下一步
完成高级进阶后,继续学习 实战精选 部分,你将看到:
- 第三方库集成示例
- 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)
- 错误处理:
do、catch、try的基本用法 - 集合类型:理解 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 判断 |
| 嵌套访问 | 需要嵌套模型或 SwiftyJSON | user.profile.bio | data["user"]["profile"]["bio"] |
| 错误处理 | do/catch | Result<T, E> | try/except |
Swift 的 Codable 和 Rust 的 serde 在思路上非常相似,都是通过派生(derive)或协议遵守(conform)来自动生成序列化代码。Python 的做法更灵活但更脆弱,所有检查都推迟到了运行期。
动手练习 Level 1
目标:用 JSONDecoder 解析一个简单的 JSON 对象。
假设你收到这样一段 JSON,里面是一本书的信息:
{"title": "Swift 编程指南", "year": 2024, "author": "李白"}
你的任务是:
- 定义一个
Book结构体,遵守Codable - 声明三个属性:
title、year、author - 用
JSONDecoder把上面的 JSON 解析成Book实例 - 在控制台打印书名和作者
点击查看答案
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 里使用驼峰命名(userName、userAge、profilePic),怎么做?
点击查看答案
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 字段名的枚举类型 |
| JSONSerialization | JSON 序列化器 | Foundation 框架提供的旧式 JSON 解析类 |
| JSONDecoder | JSON 解码器 | 将 JSON Data 解码为 Swift 类型的类 |
| JSONEncoder | JSON 编码器 | 将 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 应用,文件操作都是基础能力。
你会学到什么
完成本章后,你可以:
- 使用
FileManager获取 Documents、Caches、Temp、ApplicationSupport 等系统路径 - 理解
TemporaryFile类的 RAII 自动清理模式 - 使用
AsyncLineSequence异步逐行读取大文件 - 识别路径不存在、权限不足、跨平台差异等常见错误
- 将 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 | 缓存数据,可重新下载的网络请求结果 | 不备份 | 磁盘空间不足时 |
| Temporary | NSTemporaryDirectory() | 完全临时文件,用完即删 | 不备份 | 应用退出后 |
| 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 对比
| 概念 | Python | Rust | Swift | 关键差异 |
|---|---|---|---|---|
| 文件系统接口 | os.path / pathlib | std::fs | FileManager | Python 路径是字符串,Swift 用 URL |
| 获取 Home 目录 | os.path.expanduser("~") | home_dir() | homeDirectoryForCurrentUser | 各有封装 |
| 创建临时文件 | tempfile.NamedTemporaryFile() | 无标准库方案 | NSTemporaryDirectory() + UUID | Python 最方便 |
| 读取文件内容 | pathlib.Path.read_text() | std::fs::read_to_string() | String(contentsOf:encoding:) | Rust 需手动打开 |
| 文件不存在处理 | 抛 FileNotFoundError | Result<T, io::Error> | throws + do/catch | Rust 需要手动解 Result |
| 自动清理资源 | with 语句(上下文管理器) | Drop trait(RAII) | deinit(引用计数) | Rust 编译期保证,Swift 运行期 |
| 逐行读取大文件 | for line in f: | BufReader::lines() | url.lines async | Swift 需要 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等目录。
小结
核心要点:
- FileManager 有四大核心目录 — Documents 存用户文件,Caches 存缓存,Temporary 存临时文件,Application Support 存配置和数据库
- 临时文件用 RAII 自动清理 — 利用
deinit确保不泄露,无需手动删除 - 大文件用 AsyncLineSequence 逐行读取 — 需要 macOS 12+,避免内存占满
- 权限和路径是两类常见错误 — 先检查文件存在再读写,只写沙盒目录
- Linux 平台差异要注意 — 没有 Documents/Caches,用条件编译或 POSIX 路径
术语表
| English | 中文 | 说明 |
|---|---|---|
| FileManager | 文件管理器 | macOS/iOS 提供的文件系统操作类 |
| URL (file URL) | 文件 URL | 以 file:// 开头的统一资源定位符,比字符串路径更安全 |
| 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 就是为你准备的。
你会学到什么
完成本章后,你可以:
- 使用
@Model宏 (Macro) 声明可持久化的数据类 - 配置
ModelContainer管理 SQLite 存储路径和选项 - 使用
ModelContext执行增删改查 (CRUD) 操作 - 用
FetchDescriptor和SortDescriptor排序并取回数据 - 用
#Predicate宏写出类型安全的过滤条件 - 用
@ModelActor实现并发安全的后台数据导入 - 用
@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 // 新增字段带默认值
}
只需要:
- 新增属性提供默认值
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) // 返回 []
修复: 在 ModelContainer 的 for: 参数列出所有模型:
// ✅
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 generate | alembic 迁移工具 |
| 查询方式 | FetchDescriptor + #Predicate | 类型安全 builder API 或原生 SQL | Query 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: String、age: Int、enrolled: Date。用 ModelContainer 和 ModelContext 将三个学生数据存入数据库。
// 在 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.swift 中 MetricsDataService 的实现,创建一个 StudentImportService:
- 定义
@ModelActor actor StudentImportService - 提供
importStudents(_ names: [String]) async方法 - 提供
getStudentCount() async -> Int方法 - 在外部通过
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: ModelActor 的 modelContext 和手动创建的 ModelContext 有什么区别,为什么推荐在 Actor 中使用 @ModelActor?
查看答案和解析
@ModelActor 自动生成的 modelContext 绑定在 Actor 的隔离域内,所有对这个 context 的操作都会经过 Actor 的 serial queue 调度,不会发生并发冲突。手动创建的 ModelContext 没有这种隔离保证,如果从多个线程同时操作同一个 context 会导致数据损坏。@ModelActor 相当于帮你自动处理了线程安全。
题目 3: 你给 ServerLog 模型增加了一个 duration: Double 字段后,已安装的应用启动时崩溃。列出至少两种修复办法。
查看答案和解析
两种修复办法:
- 开发阶段:删除旧的 SQLite 文件重建。用
FileManager.default.removeItem(at: oldDBURL)或者让用户卸载重装。适用于还没有用户数据的开发/测试阶段。 - 发布阶段:启用轻量迁移。设置
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) 和基本的文件读写概念。
你会学到什么
完成本章后,你可以:
- 使用
ProcessInfo.processInfo读取操作系统级别的环境变量 - 使用
swift-dotenv加载.env文件中的配置 - 理解动态成员查找 (dynamic member lookup) 的语法糖
@dynamicMemberLookup - 明白为什么绝不应该把密钥写死在源代码里
- 排查常见的环境变量读取错误
前置要求
- 掌握 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() 则完全不同。它会:
- 在当前工作目录查找
.env文件 - 解析文件中的每一行(忽略注释和空行)
- 将解析出的键值对写入进程内存(通过
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 的可能原因:
.env文件没有加载成功(检查Dotenv.configure()是否抛出错误).env中的键名不是API_KEY(.apiKey会自动转为API_KEY,如果实际键名不同就找不到)- 值类型为
.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.apiKey 和 Dotenv["API_KEY"] 有什么区别?
功能上没有任何区别。.apiKey 是语法糖,编译器内部会自动转为 ["API_KEY"] 下标访问。命名规则是:驼峰转大写+下划线,mySecretValue → MY_SECRET_VALUE。
Q6. 如何在 Xcode 中设置环境变量?
编辑 Scheme → Run → Arguments → Environment Variables,在那里添加键值对。这种方式设置的环境变量只在通过 Xcode 运行时生效。
小结
ProcessInfo.processInfo.environment是标准库提供的环境变量读取方式,返回[String: String]字典swift-dotenv可以从.env文件加载配置到进程内存,适合管理多组环境变量@dynamicMemberLookup允许用属性访问语法读取环境变量,让代码更简洁- 敏感信息(API 密钥、数据库密码)绝不应该写死在源代码中
.env文件应加入.gitignore,避免泄露到版本控制系统
术语表
| 术语 | 说明 |
|---|---|
ProcessInfo | Swift 标准库类,封装当前进程信息,包括环境变量、参数、主机名等 |
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 项目中管理敏感信息的正确方式。接下来可以:
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 Statement | Guard 语句 | 用于提前退出,条件不满足时执行 |
函数与闭包
| 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 Clause | Where 子句 | 定义额外的类型约束条件 |
错误处理
| 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 章的学习!本章帮助你巩固所有关键概念,为高级进阶做好准备。
✅ 自检清单
完成以下每项,全部打勾后即可进入高级部分:
变量与表达式
-
能用
let和var正确声明变量 - 理解 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 并发获取多条数据
➡️ 下一步
基础部分完成后,继续学习:
你已经掌握了 Swift 的核心语法!继续前进! 🚀
高级进阶术语表
本文档收录 Swift 高级进阶部分的常用术语,供读者查阅参考。
A-E
| 中文 | 英文 | 说明 |
|---|---|---|
| 数组 | Array | 有序集合,支持索引访问 |
| 异步 | async | 异步函数声明关键字 |
| Actor | Actor | Swift 并发模型中的隔离单元,保证数据安全 |
| 编码 | Encoding | 将 Swift 类型转换为外部格式(如 JSON) |
| Codable | Codable | 编码和解码协议的组合 (Encodable + Decodable) |
| CodingKeys | CodingKeys | 自定义 JSON 键名映射的枚举 |
| 容器 | Container | SwiftData 中管理数据模型实例的对象 |
| 缓存目录 | Caches Directory | 存储临时缓存文件,系统可能清理 |
| 文档目录 | Documents Directory | 存储用户文档,iTunes 会备份 |
| 动态成员查找 | Dynamic Member Lookup | 编译时动态访问属性的特性 |
F-I
| 中文 | 英文 | 说明 |
|---|---|---|
| 描述符 | FetchDescriptor | SwiftData 查询配置对象 |
| 文件管理器 | FileManager | Foundation 文件系统操作类 |
| 强制解包 | Force Unwrap | 使用 ! 强制获取可选值(危险操作) |
| @Model | @Model | SwiftData 数据模型宏 |
| ModelActor | ModelActor | SwiftData 并发安全的 Actor 模式 |
| ModelContainer | ModelContainer | SwiftData 数据库容器 |
| ModelContext | ModelContext | SwiftData 操作上下文 |
| 迁移 | Migration | 数据模型变更时的迁移策略 |
| 不可变性 | Immutability | 常量声明后不可修改的特性 |
J-P
| 中文 | 英文 | 说明 |
|---|---|---|
| JSON 解码器 | JSONDecoder | Codable 协议的 JSON 解码工具 |
| JSON 编码器 | JSONEncoder | Codable 协议的 JSON 编码工具 |
| JSON 序列化 | JSONSerialization | Foundation 传统 JSON 解析工具 |
| 谓词 | Predicate | 数据库查询过滤条件表达式 |
| #Predicate | #Predicate | SwiftData 查询条件宏 |
| 进程信息 | ProcessInfo | 获取系统环境变量的单例 |
| 属性包装器 | Property Wrapper | 包装属性访问的自定义类型 |
Q-T
| 中文 | 英文 | 说明 |
|---|---|---|
| 查询 | Query | SwiftData SwiftUI 数据请求 |
| RAII | RAII | 资源获取即初始化,用于自动清理 |
| 关系 | Relationship | SwiftData 模型间的关联关系 |
| 排序描述符 | SortDescriptor | 查询结果的排序配置 |
| Sendable | Sendable | 跨并发边界安全传递的协议 |
| 流式读取 | Streaming Read | 异步逐行读取大文件 |
| Task | Task | Swift 并发任务的执行单元 |
| 临时文件 | Temporary File | 系统自动清理的短期文件 |
| 临时目录 | Temporary Directory | 存放临时文件的系统目录 |
U-Z
| 中文 | 英文 | 说明 |
|---|---|---|
| URL | URL | 文件路径或网络地址的表示 |
| 值类型 | Value Type | struct、enum 等复制语义的类型 |
| 等待 | await | 异步函数等待结果的运算符 |
| SwiftyJSON | SwiftyJSON | 第三方 JSON 解析库,简化访问 |
环境配置术语
| 中文 | 英文 | 说明 |
|---|---|---|
| 环境变量 | Environment Variable | 系统级配置参数 |
| .env 文件 | .env file | 项目级环境配置文件 |
| dotenv | dotenv | 加载 .env 文件的工具/库 |
| API 密钥 | API Key | 第三方服务的访问凭证 |
SwiftData 术语
| 中文 | 英文 | 说明 |
|---|---|---|
| 模型宏 | @Model macro | 将 class 转换为持久化模型的宏 |
| 持久标识符 | PersistentIdentifier | SwiftData 对象的唯一 ID |
| 内存模式 | In-Memory Mode | 不写入磁盘的数据库配置 |
| SQLite | SQLite | SwiftData 默认的存储后端 |
| 级联删除 | Cascade Delete | 关系对象的自动删除规则 |
平台术语
| 中文 | 英文 | 说明 |
|---|---|---|
| macOS 14+ | macOS 14+ | SwiftData 所需最低版本 |
| macOS 12+ | macOS 12+ | FileManager async APIs 所需版本 |
| Linux | Linux | Swift 支持的平台,部分特性受限 |
| 应用支持目录 | Application Support Directory | 存储应用配置和数据库的目录 |
返回: 高级进阶
阶段复习:高级进阶
完成高级进阶部分的四个章节后,让我们通过本复习章节巩固所学知识。
📋 知识回顾
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访问值
🧪 综合练习
练习 1: JSON + 文件 + SwiftData
任务: 编写一个应用,读取 JSON API 响应,解析数据,写入临时文件,并存储到 SwiftData。
步骤:
- 使用 JSONDecoder 解析 API 返回的用户 JSON
- 将解析结果写入临时文件(用于调试)
- 使用 @Model 定义 User 模型
- 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: 环境配置 + 文件日志
任务: 从 .env 加载 API 密钥,读取文件,记录操作日志。
步骤:
- Dotenv.configure() 加载配置
- 获取 API_KEY 值
- 使用 FileManager 写入日志文件到 Application Support
- 使用 AsyncLineSequence 读取日志验证
点击查看提示
// 1. 加载配置
try Dotenv.configure()
let apiKey = Dotenv.apiKey?.stringValue
// 2. 获取 Application Support 目录
let appSupport = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first!
// 3. 写入日志
let logFile = appSupport.appendingPathComponent("operations.log")
try logData.write(to: logFile)
// 4. 流式读取
for try await line in logFile.lines {
print("Log: \(line)")
}
✅ 知识检查
问题 1
Swift 中解析 JSON 有哪三种主要方法?各自的优缺点是什么?
点击查看答案
| 方法 | 优点 | 缺点 |
|---|---|---|
| JSONSerialization | Foundation 内置,无需额外依赖 | 返回 Any,需要手动类型转换,易出错 |
| JSONDecoder/Codable | 类型安全,编译期检查,自动映射 | 需要定义 Codable 类型,灵活性较低 |
| SwiftyJSON | 动态访问,深层嵌套简化,容错性强 | 第三方依赖,性能略低 |
问题 2
FileManager 的 Documents、Caches、Temp 目录有什么区别?
点击查看答案
| 目录 | 用途 | 备份 | 清理 |
|---|---|---|---|
| Documents | 用户文档、重要数据 | iTunes 备份 | 不会自动清理 |
| Caches | 缓存数据、下载临时文件 | 不备份 | 系统空间不足时可能清理 |
| Temp | 短期临时文件 | 不备份 | 系统重启或定期清理 |
| Application Support | 应用配置、数据库 | iTunes 备份 | 不会自动清理 |
问题 3
SwiftData 的 @ModelActor 模式解决了什么问题?如何正确使用?
点击查看答案
解决的问题:
- SwiftData 模型对象不是
Sendable,无法直接跨 Actor 传递 - 主线程数据库操作会阻塞 UI
正确使用方法:
- 定义
@ModelActoractor 类 - 在 actor 内使用
modelContext执行操作 - 返回
PersistentIdentifier而不是模型对象 - 在调用端通过 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 }
}
}
问题 4
为什么不应该硬编码 API 密钥?swift-dotenv 如何帮助解决这个问题?
点击查看答案
原因:
- 安全性:密钥泄露后被滥用
- 灵活性:开发/测试/生产环境使用不同密钥
- 代码共享:公开代码时密钥不应暴露
swift-dotenv 方案:
- 将密钥存储在
.env文件 .env文件添加到.gitignore- 使用
Dotenv.configure()加载 - 通过
Dotenv.apiKey访问 - 不同环境使用不同的
.env.production/.env.development
📈 自我评估
完成以下检查项,评估你的掌握程度:
- 能够使用 JSONDecoder 解析嵌套 JSON 并处理可选字段
- 能够使用 CodingKeys 自定义 JSON 键名映射
- 能够正确获取 Documents/Caches/Temp 目录路径
- 能够创建临时文件并理解 RAII 自动清理机制
- 能够使用 AsyncLineSequence 流式读取大文件
- 能够定义 @Model 数据类并提供初始化器
- 能够配置 ModelContainer 并执行 CRUD 操作
- 能够使用 #Predicate 和 FetchDescriptor 查询数据
- 能够使用 @ModelActor 在后台线程安全操作数据库
- 能够使用 ProcessInfo 读取系统环境变量
- 能够使用 swift-dotenv 加载 .env 文件
- 理解为什么不应该硬编码敏感配置
➡️ 下一步
完成高级进阶复习后,继续学习 实战精选 部分,你将:
- 学习第三方库集成
- 实现 LeetCode 题目
- 掌握工程化最佳实践
返回: 高级进阶概览