Swift: Swift 枚举与模式匹配教程:从基础关联值到递归枚举
枚举在 Swift 中远不止"一组常量"——它是一等公民,可以携带数据、支持方法,甚至递归定义自身,是构建状态机和结果类型的最佳工具。
1. 你将学到
- 枚举的基本语法与原始值用法
- 关联值如何让枚举携带附加数据
- 模式匹配的各种写法(switch、if-case、guard-case)
- 递归枚举的 indirect 关键字
- 枚举在状态机中的实际应用
2. 一个移动端开发者的真实故事
(1) 痛点:API 响应状态难以表达
Alice 在开发一个新闻 App 的网络层时,遇到了一个棘手问题。API 请求可能返回多种结果:
SWIFT
// 不优雅的做法:用多个可选变量表示状态
var data: NewsResponse? = nil
var error: Error? = nil
var isLoading = false
这种方式有三个问题:data 和 error 同时存在时含义模糊、忘记处理某个状态导致崩溃、代码逻辑分散难以维护。
(2) 枚举关联值的解法
用一个枚举统一表达所有状态,每个 case 携带自己的数据:
SWIFT
enum NetworkResult<T> {
case success(T)
case failure(Error)
case loading
}
NetworkResult<[NewsArticle]> 要么携带文章数据(success),要么携带错误信息(failure),要么是 loading 状态——三种状态互斥,语义清晰。
(3) 收益:状态管理从混乱到清晰
| 维度 | 多可选变量方案 | 枚举关联值方案 |
|---|---|---|
| 状态数量 | 2^N 种非法组合 | N 种精确状态 |
| 语义清晰度 | ❌ data/error 可能共存 | ✅ 每种状态自带数据 |
| 处理完整性 | ⚠️ 容易漏处理 | ✅ switch 穷举检查 |
| 代码可维护性 | 分散在 if-else 中 | ✅ 集中在一个类型 |
3. 枚举基础
枚举定义一组相关的值,Swift 的枚举比其他语言的更灵活——可以关联方法、计算属性和下标。
(1) 基本枚举与原始值
graph LR
A["enum Compass {\n case north\n case south\n case east\n case west\n}"] --> B["Compass.north"]
A --> C["Compass.south"]
A --> D["Compass.east"]
A --> E["Compass.west"]
原始值(Raw Value)是枚举 case 背后的固定值,可以是 String、Int 等类型。
| 原始值类型 | 声明方式 | 值 |
|---|---|---|
| Int | enum Status: Int { case ok = 200 } |
整数 |
| String | enum Direction: String { case north = "N" } |
字符串 |
| 隐式 Int | enum Grade: Int { case a, b, c } |
从 0 开始递增 |
| 隐式 String | enum Code: String { case red, green } |
与 case 名相同 |
▶ 示例:HTTP 状态码枚举
SWIFT
// ============================================
// 使用原始值的 HTTP 状态码枚举
// ============================================
enum HTTPStatus: Int {
case ok = 200
case created = 201
case badRequest = 400
case unauthorized = 401
case notFound = 404
case serverError = 500
}
let status = HTTPStatus.notFound
print("Code: \(status.rawValue)")
print("Name: \(status)")
// 从原始值反向创建
let parsed = HTTPStatus(rawValue: 200)
print("Parsed: \(parsed ?? .serverError)")
输出:
TEXT 📖 仅展示Code: 404 Name: notFound Parsed: ok
(2) 枚举方法与计算属性
枚举可以像结构体一样定义方法和属性。
▶ 示例:带方法的枚举
SWIFT
// ============================================
// 枚举自定义方法:判断状态类别
// ============================================
enum HTTPStatus: Int {
case ok = 200
case created = 201
case badRequest = 400
case unauthorized = 401
case notFound = 404
case serverError = 500
var isSuccess: Bool {
rawValue >= 200 && rawValue < 300
}
var description: String {
switch self {
case .ok: return "OK"
case .created: return "Created"
case .badRequest: return "Bad Request"
case .unauthorized: return "Unauthorized"
case .notFound: return "Not Found"
case .serverError: return "Internal Server Error"
}
}
}
let code = HTTPStatus.ok
print("\(code.rawValue) \(code.description) - Success: \(code.isSuccess)")
输出:
TEXT 📖 仅展示200 OK - Success: true
4. 关联值与模式匹配
关联值是枚举区别于其他语言的核心特性——每个 case 可以携带不同类型的附加数据。
(1) 关联值语法
graph TB
A["NetworkResult<T>"] --> B["success(T) - 携带数据"]
A --> C["failure(Error) - 携带错误"]
A --> D["loading - 无数据"]
B --> E["switch result {\n case .success(let data):\n show(data)\n case .failure(let error):\n show(error)\n case .loading:\n showSpinner()\n}"]
| 特性 | 原始值 | 关联值 |
|---|---|---|
| 值类型 | 固定常量 | 可变数据 |
| 存储位置 | 定义时确定 | 创建时传入 |
| 每个 case | 都有相同类型的值 | 每个 case 可以不同 |
| 使用场景 | 映射固定编码 | 携带动态数据 |
▶ 示例:网络请求结果处理
SWIFT
// ============================================
// 关联值枚举处理 API 响应
// ============================================
enum NetworkResult<T> {
case success(T)
case failure(String)
case loading
}
func handleResponse<T>(_ result: NetworkResult<T>) {
switch result {
case .success(let data):
print("成功获取数据: \(data)")
case .failure(let error):
print("请求失败: \(error)")
case .loading:
print("加载中...")
}
}
handleResponse(NetworkResult.success("用户信息"))
handleResponse(NetworkResult.failure("网络连接超时"))
handleResponse(NetworkResult.loading)
输出:
TEXT 📖 仅展示成功获取数据: 用户信息 请求失败: 网络连接超时 加载中...
(2) if-case 与 guard-case 模式匹配
除了 switch,还可以用 if case 和 guard case 进行单 case 匹配。
▶ 示例:简洁的单 case 匹配
SWIFT
// ============================================
// if-case 和 guard-case 的简洁用法
// ============================================
enum Measurement {
case weight(Double)
case height(Double)
case count(Int)
}
let record = Measurement.weight(75.5)
// if-case 只关心一种情况
if case .weight(let kg) = record, kg > 70 {
print("体重大于 70kg: \(kg)")
}
// guard-case 提前退出
func process(_ m: Measurement) {
guard case .count(let n) = m else {
print("不是计数类型")
return
}
print("计数为 \(n)")
}
process(.weight(65))
process(.count(42))
输出:
TEXT 📖 仅展示体重大于 70kg: 75.5 不是计数类型 计数为 42
5. 递归枚举
递归枚举指枚举的关联值又引用了自身,适合表达树状或嵌套结构。
(1) indirect 关键字
用 indirect 标记递归 case 或整个枚举,告知编译器需要间接存储。
| 写法 | 说明 |
|---|---|
indirect case expression(Expression) |
单个 case 递归 |
indirect enum Expression { ... } |
整个枚举允许递归 |
▶ 示例:数学表达式求值
SWIFT
// ============================================
// 递归枚举:数学表达式树
// ============================================
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
func evaluate(_ expr: ArithmeticExpression) -> Int {
switch expr {
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)
}
}
// 构建表达式 (3 + 5) * 2
let three = ArithmeticExpression.number(3)
let five = ArithmeticExpression.number(5)
let sum = ArithmeticExpression.addition(three, five)
let product = ArithmeticExpression.multiplication(sum, .number(2))
print("(3 + 5) * 2 = \(evaluate(product))")
输出:
TEXT 📖 仅展示(3 + 5) * 2 = 16
6. 完整示例:订单状态机
SWIFT
// ============================================
// 完整示例:电商订单状态机
// 功能:订单状态流转 + 数据处理
// ============================================
import Foundation
// 1. 订单状态枚举(关联值)
enum OrderStatus {
case pending // 待支付
case paid(Date) // 已支付(含支付时间)
case shipped(trackingNumber: String) // 已发货(含运单号)
case delivered(Date) // 已送达(含签收时间)
case cancelled(reason: String) // 已取消(含原因)
var description: String {
switch self {
case .pending: return "待支付"
case .paid(let date): return "已支付 (\(date.formatted()))"
case .shipped(let tracking): return "已发货 (运单: \(tracking))"
case .delivered(let date): return "已送达 (\(date.formatted()))"
case .cancelled(let reason): return "已取消: \(reason)"
}
}
var isFinalState: Bool {
if case .delivered = self { return true }
if case .cancelled = self { return true }
return false
}
}
// 2. 使用示例
let now = Date()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let statuses: [OrderStatus] = [
.pending,
.paid(now),
.shipped(trackingNumber: "SF1234567890"),
.delivered(now),
.cancelled(reason: "用户取消")
]
for status in statuses {
let final = status.isFinalState ? "(终态)" : ""
print("\(status.description) \(final)")
}
输出:
TEXT 📖 仅展示待支付 已支付 (2026-07-30) 已发货 (运单: SF1234567890) 已送达 (2026-07-30) (终态) 已取消: 用户取消 (终态)
❓ 常见问题
Q 枚举和结构体如何选择?
A 枚举适合"一组互斥状态"——某个时间点只能是其中一种;结构体适合"多个属性共存"——所有属性同时存在。选哪个取决于你的数据模型是"或"关系还是"与"关系。
Q 关联值和泛型能不能一起用?
A 可以。
enum Optional<T> 就是关联值 + 泛型的经典例子:case none 或 case some(T)。关联值可以引用泛型参数。Q 为什么 Swift 枚举比 C/Java 的 enum 强大?
A Swift 枚举可以有方法、计算属性、关联值、递归定义、遵守协议。C/Java 的 enum 只是命名的整数常量,Swift 枚举是完整的一等类型。
Q 什么时候必须用 indirect?
A 当枚举的关联值类型包含自身时。例如
ArithmeticExpression.addition 的关联值是 ArithmeticExpression,形成递归引用,必须 indirect。Q switch 匹配枚举时一定要写 default 吗?
A 推荐穷举所有 case,不要用 default。这样未来新增 case 时,编译器会警告遗漏,避免漏处理。但如果确实不需要处理所有 case,可以加 default。
📖 小节
- Swift 枚举是一等类型,支持方法、计算属性和协议遵守
- 原始值(Raw Value)为每个 case 绑定固定常量值
- 关联值让每个 case 携带不同类型的附加数据
- 模式匹配(switch、if-case、guard-case)安全地提取枚举数据
- 递归枚举用 indirect 关键字表达树状嵌套结构
- 枚举是构建状态机、结果类型和可选类型的理想工具
📝 作业
- 基础题: 定义一个
TrafficLight枚举,包含 red、yellow、green 三个 case,并添加一个duration计算属性返回每种灯光的持续秒数。 - 进阶题: 使用关联值定义一个
Either<L, R>枚举(类似 Rust 的 Result),包含left(L)和right(R)两个 case,并添加map方法转换 right 值。 - 挑战题: 用递归枚举实现一个 JSON 类型(支持 null、bool、int、string、array、dictionary),并编写一个函数将 JSON 枚举格式化为缩进字符串。