1. 在 switch 中提取 associated values
当你第一次看到带有 associated values 的 enum 时,会很想像访问结构体字段那样访问数据: command.title、 command.index 等等。但 enum 的工作方式更巧妙:它的并非所有字段都会一直存在,因为在任何时刻只有一个 case 处于激活状态。 所以 Swift 会先要求我们回答一个问题:“当前是哪一个 case?”——只有在这之后,才会把该 case 的数据交给我们。
这时 switch 就不只是一个分支语句了,而是真正的“解包器”: 它会同时检查变体,并把关联值稳稳地提取到本地常量/变量中。 在 Swift 的官方示例里,这种写法非常自然: switch self { case let .load(key): ... } ——也就是“如果是 .load,就把 key 取出来”。
基本形式:case let .someCase(value)
通常在练习题里,一开始都会有一个小小的 enum,表示“程序命令”。 我们来做一个这样的模型:面向迷你图书馆的命令(我们会逐步过渡到 CLI 应用,也就是用户通过文本输入命令)。 不同命令携带的数据不同:add 有书名,remove 有索引,而 list 则没有数据。
import Foundation
enum LibraryCommand {
case add(title: String)
case remove(index: Int)
case list
}
let cmd: LibraryCommand = .add(title: "Clean Code")
print(cmd) // add(title: "Clean Code")
现在我们想把这个命令“描述”为字符串。这时经典写法就出现了: case let .add(title):
import Foundation
enum LibraryCommand {
case add(title: String)
case remove(index: Int)
case list
}
func describe(_ command: LibraryCommand) -> String {
switch command {
case let .add(title):
return "ADD: \(title)"
case let .remove(index):
return "REMOVE: #\(index)"
case .list:
return "LIST"
}
}
print(describe(.remove(index: 2))) // REMOVE: #2
注意这里那种“读起来像一句话”的感觉: case let .remove(index) ——“当它是 remove 时,把 index 取出来”。你会在标准库示例和 Swift Evolution 提案中看到同样的风格。
两种等价写法
当你搜索示例时,会看到两种提取风格。它们做的是同一件事,差别只是语法形式——看谁读起来更顺手。 认识这两种写法很有用,这样你就不会想“这是不是另一种 Swift”。
写法 A(Swift 代码里最常见): case let .add(title):
写法 B(更“显式”): case .add(let title):
import Foundation
enum LibraryCommand { case add(title: String) }
func extractTitle(_ command: LibraryCommand) -> String {
switch command {
case let .add(title):
return title
}
}
func extractTitle2(_ command: LibraryCommand) -> String {
switch command {
case .add(let title):
return title
}
}
print(extractTitle(.add(title: "Dune"))) // Dune
print(extractTitle2(.add(title: "Dune"))) // Dune
实践建议:选一种风格,并在项目里保持一致。 在后面的课程里,我们通常会写 case let ...,因为它更短,也更常见。
一次提取多个关联值
一旦某个 case 带有多个参数(例如“把书从 from 位置移动到 to 位置”), 提取方式依然很简单:在模式中按相同顺序列出这些值。
重要的是:标签(labels)能帮助人避免把参数搞混。 这不只是好看,更是在预防“代码能编译,但行为不对”这一类 bug。
import Foundation
enum LibraryCommand {
case move(from: Int, to: Int)
}
func describe(_ command: LibraryCommand) -> String {
switch command {
case let .move(from, to):
return "MOVE: \(from) → \(to)"
}
}
print(describe(.move(from: 1, to: 3))) // MOVE: 1 → 3
这里我们写了 case let .move(from, to) 而没有显式标签——Swift 允许这样做,因为匹配是按位置进行的。 不过有时显式标签会让代码更稳妥,尤其是当你不确定自己会不会把值写反(或者读代码的人不容易看懂)时。
import Foundation
enum LibraryCommand {
case move(from: Int, to: Int)
}
func describe(_ command: LibraryCommand) -> String {
switch command {
case let .move(from: fromIndex, to: toIndex):
return "MOVE: \(fromIndex) → \(toIndex)"
}
}
print(describe(.move(from: 2, to: 0))) // MOVE: 2 → 0
Swift Evolution 里专门讨论过:如果在模式中对名称/标签要求太死,会降低 ergonomics, 所以语言会努力保持平衡:既可以写得更短,也可以写得更“自说明”。
_:忽略一部分 payload
在真实代码里,你经常会遇到这样的情况:一个 case 有两个值,但你只需要其中一个。 为了不用的变量去创建一个局部绑定,是很奇怪的做法(编译器也可能会提醒你)。 所以模式里有 _:表示“我知道这里有东西,但我不需要它”。
假设有一个“按索引替换书籍”的命令:索引需要,但旧书名(假设它也被传进来)并不需要。
import Foundation
enum LibraryCommand {
case replace(index: Int, oldTitle: String, newTitle: String)
}
func newTitleOnly(_ command: LibraryCommand) -> String {
switch command {
case let .replace(_, _, newTitle):
return newTitle
}
}
print(newTitleOnly(.replace(index: 1, oldTitle: "A", newTitle: "B"))) // B
注意这里三个 _ 位置:我们实际上是在说“第一个和第二个值忽略掉,只取第三个”。 这比为了不用而去写 let unused = ... 然后假装没人会问“那它干嘛用”要诚实得多。
没有 default 的 switch:为什么它是你的朋友
当针对 enum 的 switch 是穷尽式的(也就是列出了所有 case)时, Swift 就会变成一个严格但公平的审查者:如果你新增了一个 case,编译器会强迫你更新所有处理这个 enum 的地方。 这是 Swift 里最令人愉快的“防 bug”机制之一。
在 Swift 的官方资料(以及语言演进提案)中,针对 enum 的 switch 通常都会写成穷尽式,而不是塞一个 default “兜底”,这样才能保住这个保证。
import Foundation
enum LibraryCommand {
case list
case help
}
func handle(_ command: LibraryCommand) {
switch command {
case .list:
print("Listing...") // Listing...
case .help:
print("Help...") // Help...
}
}
handle(.help)
如果明天你增加了 case add(title: String) ——编译器会诚实地告诉你:“嘿,这里的 switch 已经不穷尽了。”这正是好事。
where in case:无需嵌套 if 的特殊情况
有时只知道“是哪一个 case”还不够:你还需要看它的数据。 比如命令 .remove(index: -1) 形式上是对的(它仍然是 .remove),但语义上却是无效的。 你可以在分支内部用 if 处理,也可以通过 where 直接写进模式里。
where 的核心想法很简单:这个分支只有在 case 匹配并且条件为真时才会执行。 这样一来,switch 仍然保持可读:上面写特殊情况,下面写通用情况。
import Foundation
enum LibraryCommand {
case remove(index: Int)
}
func validate(_ command: LibraryCommand) -> String {
switch command {
case let .remove(index) where index < 0:
return "Index must be >= 0"
case let .remove(index):
return "Removing at #\(index)"
}
}
print(validate(.remove(index: -2))) // Index must be >= 0
print(validate(.remove(index: 5))) // Removing at #5
这里要讲究分寸:where 不是写“检查五个字段、两个数组,再顺便看看月亮是不是在天蝎座”的地方。 如果条件变得很长,最好把分支内的逻辑写清楚,或者把检查提取到单独的函数里。
3. 速查表和实战示例
关于提取的速查表
在我们组装迷你命令处理器之前,先在脑子里放一张简洁的“语法地图”。 编程里的表格就像注释:有时能救你于愚蠢错误之中。
| 我们想做什么 | 在 switch 里怎么写 | 会得到什么 |
|---|---|---|
| 只按不带数据的 case 匹配 | |
只是进入该分支 |
| 提取一个值 | |
在分支内可用 |
| 另一种写法 | |
结果相同 |
| 提取两个值 | |
、 可用 |
| 忽略部分数据 | |
可用, 被忽略 |
| 按数据处理特殊情况 | |
只有在条件满足时才会进入分支 |
迷你示例:通过 switch 处理命令
现在我们来做一个小而真实感更强的片段:这里有一个书籍数组(先简单地用 [String] 表示), 还有一个 LibraryCommand。我们希望有一个 apply 函数,它接收命令并修改状态(数组), 同时返回一段可以展示给用户的文本。
请注意 switch 在这里多么合适:命令本身就是一组可选情况中的一个,而每个情况都需要不同的逻辑。
import Foundation
enum LibraryCommand {
case add(title: String)
case remove(index: Int)
case list
}
func apply(_ command: LibraryCommand, books: inout [String]) -> String {
switch command {
case let .add(title) where title.isEmpty:
return "Title can't be empty"
case let .add(title):
books.append(title)
return "Added: \(title)"
case .list where books.isEmpty:
return "Library is empty"
case .list:
return "Books: \(books.joined(separator: ", "))"
case let .remove(index) where index < 0 || index >= books.count:
return "Invalid index: \(index)"
case let .remove(index):
let removed = books.remove(at: index)
return "Removed: \(removed)"
}
}
var books: [String] = []
print(apply(.list, books: &books)) // Library is empty
print(apply(.add(title: "Dune"), books: &books)) // Added: Dune
print(apply(.list, books: &books)) // Books: Dune
这里把本讲的“主角”都汇聚到了一起。
- 我们通过 case let ... 提取 title 和 index
- 我们避免使用 default,从而得到一个穷尽式的 switch(如果新增命令,编译器会提醒我们补上处理)
- 我们使用 where 来突出特殊情况(空标题、空书库、非法索引),而不需要再套额外的 if
而且,这些依然只是小函数。我们不是在写一个 200 行的“上帝函数”,因为那样不管是现在还是一周后,读起来都会很痛苦。
4. 常见错误
错误 1:把 associated values 当成属性来访问。
在经过 struct 式思维训练之后,人们总想写 command.title 或者 command.index。 但 enum 并不保证每一种状态下都有这个字段。 所以在 Swift 中,associated values 是通过模式匹配提取的:通过 switch (或者通过点式的 if case/guard case, 不过那会留到下一讲)。 如果你发现自己想“把字段做成 optional 然后把所有东西都塞进一个 case”,这通常意味着模型该重构了。
错误 2:为了“能编译”而写 default,然后忘记更新处理逻辑。
default 很诱人:新增一个 case,代码也“不报错”了。 但这就像拿胶带把 “Check Engine” 指示灯粘住:声音小了,问题却还在。 在我们的命令处理器里,最好让编译器逼你把逻辑补全;否则就会出现“新命令悄悄什么都不做”,然后你还得花很久去找原因。
错误 3:提取了值,却没有使用它。
如果你写了 case let .add(title): 而后面又不用 title,编译器可能会警告,读者也会怀疑这里是不是少写了什么逻辑。 这种情况下,最好老老实实使用 _,或者只提取真正需要的内容。 在模式里使用 _ 是正常工具,不是“能力不足”的标志。
错误 4:在没有标签的 case 中把值顺序搞混。
当你写 case let .move(a, b) 时, 你必须记住 a 是 from,而 b 是 to。 在小例子里这没问题,但在真实项目里,“把参数顺序写反”是经典的逻辑错误。 如果你觉得有风险,就在 case 声明中使用标签,并在提取时使用更清晰的名字。 语言之所以支持多种形式,就是因为可读性比省下三个字符更重要。
错误 5:把 where 写成一个 5 行的小小说。
当条件很短,并且是在解释“为什么这是特殊情况”时,where 很好用。 如果条件变得很长,switch 就会变成一团复杂谜题。 这种情况下,最好把检查提取到一个命名清晰的独立函数中,然后在分支内部或直接在 where 中使用它 (但前提是它看起来像 where isValidIndex(index, books),而不是一篇数学论文)。
GO TO FULL VERSION