前言
在 C/Objective-C 里,枚举只是一组别名整型;在 Swift 里,枚举被提升为"一等类型"(first-class type),可以拥有
- 计算属性
- 实例方法
- 初始化器
- 扩展、协议、泛型
- 递归结构
因此,它不再只是"常量集合",而是一种强大的建模工具。
基础语法:enum、case、点语法
swift
// 1. 最简形式:不附带任何值
enum CompassPoint {
case north
case south
case east
case west
}
// 2. 单行多 case 写法
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
// 3. 类型推断下的点语法
var direction = CompassPoint.north
direction = .west // 类型已知,可省略前缀
与 switch 联用: 穷举检查
Swift 的 switch 必须覆盖所有 case,否则编译失败------这是"安全第一"的体现。
swift
var direction = CompassPoint.north
switch direction {
case .north:
print("Lots of planets have a north")
case .south:
print("Watch out for penguins")
case .east, .west: // 多 case 合并
print("Where the sun rises or sets")
}
// 如果注释掉任意 case,编译器立即报错
遍历所有 case:CaseIterable 协议
只需加一句 : CaseIterable
,编译器自动合成 allCases
数组。
swift
enum Beverage: CaseIterable {
case coffee, tea, juice
}
print("总共 \(Beverage.allCases.count) 种饮品")
for drink in Beverage.allCases {
print("今天喝\(drink)")
}
关联值(Associated Values)
区别于原始值,关联值是把额外信息绑定到具体实例,而不是枚举定义本身。
swift
enum Barcode {
// UPC 一维码:四段数字
case upc(Int, Int, Int, Int)
// QR 二维码:任意长度字符串
case qrCode(String)
}
// 创建实例时才真正携带值
var product = Barcode.upc(8, 85909, 51226, 3)
product = .qrCode("https://swift.org")
// switch 提取关联值
switch product {
//case .upc(let numSystem, let manufacturer, let product, let check):
// 简写:如果全是 let 或 var,可移到前面
case let .upc(numSystem, manufacturer, product, check):
print("UPC: \(numSystem)-\(manufacturer)-\(product)-\(check)")
case .qrCode(let url):
print("QR 内容: \(url)")
}
原始值(Raw Values)------"编译期就确定"
原始值与关联值互斥:
- 原始值在定义时就写死,所有实例共用;
- 关联值在创建时才给出,每个实例可以不同。
- 手动指定
swift
enum ASCIIControl: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
- 隐式自动递增 / 隐式字符串
swift
enum PlanetInt: Int {
case mercury = 1 // 显式从 1 开始
case venus // 隐式 2
case earth // 隐式 3
}
enum CompassString: String {
case north // 隐式 rawValue = "north"
case south
}
- 通过 rawValue 初始化?返回的是可选值
swift
enum PlanetInt: Int {
case mercury = 1 // 显式从 1 开始
case venus // 隐式 2
case earth // 隐式 3
}
let possiblePlanet = PlanetInt(rawValue: 7) // nil,因为没有第 7 颗行星
print(possiblePlanet) // nil
if let planet = PlanetInt(rawValue: 3) {
print("第 3 颗行星是 \(planet)") // earth
}
自定义构造器 / 计算属性 / 方法
枚举也能"长得像类"。
swift
enum LightBulb {
case on(brightness: Double) // 关联值
case off
// 计算属性
var isOn: Bool {
switch self {
case .on: return true
case .off: return false
}
}
// 实例方法
mutating func toggle() {
switch self {
case .on(let b):
self = .off
print("从亮度 \(b) 关灯")
case .off:
self = .on(brightness: 1.0)
print("开灯到默认亮度")
}
}
}
var bulb = LightBulb.on(brightness: 0.8)
bulb.toggle() // 关灯
bulb.toggle() // 开灯
递归枚举(Indirect Enumeration)
当枚举的关联值再次包含自身时,需要显式标记 indirect
,让编译器插入间接层,避免无限嵌套导致内存无法布局。
swift
// 方式 A:单个 case 递归
enum ArithmeticExpr {
case number(Int)
indirect case addition(ArithmeticExpr, ArithmeticExpr)
indirect case multiplication(ArithmeticExpr, ArithmeticExpr)
}
// 方式 B:整个枚举全部 case 都递归
indirect enum Tree<T> {
case leaf(T)
case node(Tree<T>, Tree<T>)
}
构建与求值:把"(5 + 4) * 2"装进枚举
swift
let five = ArithmeticExpr.number(5)
let four = ArithmeticExpr.number(4)
let two = ArithmeticExpr.number(2)
let sum = ArithmeticExpr.addition(five, four)
let product = ArithmeticExpr.multiplication(sum, two)
// 递归求值
func evaluate(_ expr: ArithmeticExpr) -> 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)
}
}
print(evaluate(product)) // 18
实战 1:用枚举建模"JSON"
swift
enum JSON {
case string(String)
case number(Double)
case bool(Bool)
case null
case array([JSON])
case dictionary([String: JSON])
}
let json: JSON = .dictionary([
"name": .string("Swift"),
"year": .number(2014),
"awesome": .bool(true),
"tags": .array([.string("iOS"), .string("macOS")])
])
优势:
- 编译期保证类型组合合法;
- 写解析/生成器时,switch 覆盖所有 case 即可,无需 if-else 层层判断。
实战 2:消除"字符串驱动"------网络请求路由
swift
enum API {
case login(user: String, pass: String)
case userInfo(id: Int)
case articleList(page: Int, pageSize: Int)
}
extension API {
var host: String { "https://api.example.com" }
var path: String {
switch self {
case .login: return "/login"
case .userInfo(let id): return "/users/\(id)"
case .articleList: return "/articles"
}
}
var parameters: [String: Any] {
switch self {
case .login(let u, let p):
return ["username": u, "password": p]
case .userInfo:
return [:]
case .articleList(let page, let size):
return ["page": page, "pageSize": size]
}
}
}
// 使用
let request = API.login(user: "alice", pass: "123456")
print("请求地址:\(request.host + request.path)")
好处:
- 路由与参数封装在一起,外部无需硬编码字符串;
- 新增接口只需再加一个 case,编译器会强制你补全 path & parameters。
性能与内存Tips
- 不带关联值的枚举 = 一个整型大小,最省内存。
- 关联值会占用更多空间,编译器会按最大 case 对齐;如果内存敏感,可用
indirect
将大数据挂到堆上。 - 原始值并不会额外占用存储,它只是编译期常量;运行时通过
rawValue
访问即可。 - 枚举是值类型,跨线程传递无需担心引用计数,但大体积关联值复制时要注意写时复制(CoW)开销。
给枚举加"泛型"------一个类型参数打通所有关联值
swift
// 1. 泛型枚举:Success 与 Failure 的具体类型由使用方决定
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
// 2. 网络层统一返回
enum APIError: Error { case timeout, invalidJSON }
func fetchUser(id: Int) -> Result<User, APIError> {
...
return .success(user)
}
// 3. 调用方用 switch 就能拿到强类型的 User 或 APIError
let r = fetchUser(id: 1)
switch r {
case .success(let user):
print(user.name)
case .failure(let error):
print(error)
}
要点
- 枚举可以带泛型参数,且每个 case 可使用不同参数。
- Swift 标准库已内置
Result<Success, Failure>
,无需自己写。
枚举也遵守协议------让"一组无关类型"共享行为
swift
protocol Describable { var desc: String { get } }
enum IOAction: Describable {
case read(path: String)
case write(path: String, data: Data)
var desc: String {
switch self {
case .read(let p): return "读取 \(p)"
case .write(let p, _): return "写入 \(p)"
}
}
}
let action = IOAction.write(path: "/tmp/a.txt", data: Data())
print(action.desc)
进阶:把枚举当成"小而美"的命名空间,里面再套结构体、类,一并遵守协议,可组合出非常灵活的对象图。
@unknown default ------ 面向库作者的"向后兼容"保险
当模块使用 library evolution
(BUILD_LIBRARY_FOR_DISTRIBUTION = YES
)打开 resilient 构建时,公开枚举默认是"非冻结"的,未来可能新增 case。
客户端必须用 @unknown default:
兜底,否则升级库后会得到编译警告:
swift
// 在 App 代码里
switch frameworkEnum {
case .oldCaseA: ...
case .oldCaseB: ...
@unknown default: // 少了就会警告
assertionFailure("请适配新版本 SDK")
}
冻结枚举(@frozen)则告诉编译器"以后绝对不会再加 case",可以省略 @unknown default
。
System 框架里大量使用了该技巧,保证 Apple 加新枚举值时老 App 不会直接崩溃。
SwiftUI 视图工厂------用枚举消灭"字符串驱动"的 Navigation
swift
enum Route: Hashable {
case home
case article(id: Int)
case settings(debug: Bool)
}
@main
struct App: SwiftUI.App {
var body: some Scene {
WindowGroup {
ContentView()
.navigationDestination(for: Route.self) { route in
switch route {
case .home: HomeView()
case .article(let id): ArticleView(id: id)
case .settings(let debug): SettingsView(debug: debug)
}
}
}
}
}
优点
- 路由表即枚举,强类型;
- 新增 case 编译器会强制你补全对应视图;
- 支持
NavigationStack
的path
参数,可持久化/还原整棵导航树。
把枚举当"位掩码"------OptionSet 的本质仍是枚举
swift
struct FilePermission: OptionSet {
let rawValue: Int
// 内部用静态枚举常量,对外却是结构体
static let read = FilePermission(rawValue: 1 << 0)
static let write = FilePermission(rawValue: 1 << 1)
static let execute = FilePermission(rawValue: 1 << 2)
}
let rw: FilePermission = [.read, .write]
print(rw.rawValue) // 3
为什么不用"纯枚举"?
- 枚举无法表达"组合"语义;
OptionSet
协议要求struct
以便支持按位或/与运算。
结论:需要位运算时,用结构体包一层 rawValue,而不是直接上枚举。
性能压测:100 万个关联值到底占多少内存?
测试模型
swift
enum Node {
case leaf(Int)
indirect case node(Node, Node)
}
在 64 位下
leaf
:实际 9 字节(1 字节区分 case + 8 字节 Int),但按 16 字节对齐。node
:额外存储两个指针(16 字节)+ 1 字节 tag → 24 字节对齐。
结论
- 不带
indirect
的枚举=最省内存; - 大数据字段务必
indirect
挂到堆上,避免栈爆炸; - 如果 case 差异巨大,考虑"枚举 + 类"混合:枚举负责分派,类负责存数据。
什么时候该把"枚举"改回"结构体/类"
- case 数量会动态膨胀(如用户标签、城市字典)→ 用字典或数据库。
- 需要存储大量同质数据 → 结构体数组更合适。
- 需要继承/多态扩展 → 用协议 + 类/结构体。
- 需要弱引用、循环引用 → class + delegate 模式。
口诀:"有限状态用枚举,无限集合用集合;行为多态用协议,生命周期用类。"
一条龙完整示例:用枚举写个"小型正则表达式"引擎
swift
indirect enum Regex {
case literal(Character)
case concatenation(Regex, Regex)
case alternation(Regex, Regex) // "或"
case repetition(Regex) // 闭包 *
}
// 匹配函数
extension Regex {
func match(_ str: String) -> Bool {
var idx = str.startIndex
return matchHelper(str, &idx) && idx == str.endIndex
}
private func matchHelper(_ str: String, _ idx: inout String.Index) -> Bool {
switch self {
case .literal(let ch):
guard idx < str.endIndex, str[idx] == ch else { return false }
str.formIndex(after: &idx)
return true
case .concatenation(let left, let right):
let tmp = idx
return left.matchHelper(str, &idx) && right.matchHelper(str, &idx) || ({ idx = tmp; return false })()
case .alternation(let left, let right):
let tmp = idx
return left.matchHelper(str, &idx) || ({ idx = tmp; return right.matchHelper(str, &idx) })()
case .repetition(let r):
let tmp = idx
while r.matchHelper(str, &idx) { }
return true
}
}
}
// 测试
let pattern = Regex.repetition(.alternation(.literal("a"), .literal("b")))
print(pattern.match("abba")) // true
亮点
- 纯值类型,线程安全;
- 用枚举递归描述语法树,代码即文档;
- 若需性能,可再包一层 JIT 或转成 NFA/DFA。
总结与扩展场景
- 枚举是值类型,但拥有近似类的能力。
- 关联值 = 运行期动态绑定;原始值 = 编译期静态绑定。
switch
必须 exhaustive,借助CaseIterable
可遍历。- 可以写构造器、计算属性、方法、扩展、协议等
- 建模"有限状态 + 上下文"时,优先用枚举:
- 播放器状态:
.idle / .loading(url) / .playing(item, currentTime) / .paused(item, currentTime)
- 订单状态:
.unpaid(amount) / .paid(date) / .shipped(tracking) / .refunded(reason)
- 播放器状态:
- 把"字符串魔法"改成枚举,可让编译器帮你检查漏掉的 case,减少运行时崩溃。
- 递归枚举天生适合表达树/表达式这类"自相似"结构,配合模式匹配写解释器极其清爽。
- 如果 case 太多(>100),可读性下降,可考虑:
- 拆成多级枚举(namespace)
- 用静态工厂方法隐藏细节
- 改用结构体 + 协议,让"类型"退化为"数据"
checklist:如何写"优雅"的 Swift 枚举
☑ 名字首字母大写,case 小写。
☑ 先问自己"状态是否有限",再决定用枚举还是字符串。
☑ 关联值 > 3 个字段就封装成结构体,保持 switch 整洁。
☑ 公开库一定要想好"未来会不会加 case",决定 @frozen
与否。
☑ 超过 20 个 case 考虑分层:外层命名空间枚举,内层再拆。
☑ 需要 Codable 时,关联值枚举要自定义 init(from:)
& encode(to:)
,否则编译器会报错。
☑ 最后写单元测试:把每个 case 都 switch 一遍,防止后续改挂。