Swift 枚举完全指南——从基础语法到递归枚举的渐进式学习笔记

前言

在 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)------"编译期就确定"

原始值与关联值互斥:

  • 原始值在定义时就写死,所有实例共用;
  • 关联值在创建时才给出,每个实例可以不同。
  1. 手动指定
swift 复制代码
enum ASCIIControl: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}
  1. 隐式自动递增 / 隐式字符串
swift 复制代码
enum PlanetInt: Int {
    case mercury = 1      // 显式从 1 开始
    case venus            // 隐式 2
    case earth            // 隐式 3
}

enum CompassString: String {
    case north            // 隐式 rawValue = "north"
    case south
}
  1. 通过 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

  1. 不带关联值的枚举 = 一个整型大小,最省内存。
  2. 关联值会占用更多空间,编译器会按最大 case 对齐;如果内存敏感,可用 indirect 将大数据挂到堆上。
  3. 原始值并不会额外占用存储,它只是编译期常量;运行时通过 rawValue 访问即可。
  4. 枚举是值类型,跨线程传递无需担心引用计数,但大体积关联值复制时要注意写时复制(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 evolutionBUILD_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 不会直接崩溃。

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 编译器会强制你补全对应视图;
  • 支持 NavigationStackpath 参数,可持久化/还原整棵导航树。

把枚举当"位掩码"------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 差异巨大,考虑"枚举 + 类"混合:枚举负责分派,类负责存数据。

什么时候该把"枚举"改回"结构体/类"

  1. case 数量会动态膨胀(如用户标签、城市字典)→ 用字典或数据库。
  2. 需要存储大量同质数据 → 结构体数组更合适。
  3. 需要继承/多态扩展 → 用协议 + 类/结构体。
  4. 需要弱引用、循环引用 → 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。

总结与扩展场景

  1. 枚举是值类型,但拥有近似类的能力。
  2. 关联值 = 运行期动态绑定;原始值 = 编译期静态绑定。
  3. switch 必须 exhaustive,借助 CaseIterable 可遍历。
  4. 可以写构造器、计算属性、方法、扩展、协议等
  5. 建模"有限状态 + 上下文"时,优先用枚举:
    • 播放器状态:.idle / .loading(url) / .playing(item, currentTime) / .paused(item, currentTime)
    • 订单状态:.unpaid(amount) / .paid(date) / .shipped(tracking) / .refunded(reason)
  6. 把"字符串魔法"改成枚举,可让编译器帮你检查漏掉的 case,减少运行时崩溃。
  7. 递归枚举天生适合表达树/表达式这类"自相似"结构,配合模式匹配写解释器极其清爽。
  8. 如果 case 太多(>100),可读性下降,可考虑:
    • 拆成多级枚举(namespace)
    • 用静态工厂方法隐藏细节
    • 改用结构体 + 协议,让"类型"退化为"数据"

checklist:如何写"优雅"的 Swift 枚举

☑ 名字首字母大写,case 小写。

☑ 先问自己"状态是否有限",再决定用枚举还是字符串。

☑ 关联值 > 3 个字段就封装成结构体,保持 switch 整洁。

☑ 公开库一定要想好"未来会不会加 case",决定 @frozen 与否。

☑ 超过 20 个 case 考虑分层:外层命名空间枚举,内层再拆。

☑ 需要 Codable 时,关联值枚举要自定义 init(from:) & encode(to:),否则编译器会报错。

☑ 最后写单元测试:把每个 case 都 switch 一遍,防止后续改挂。

相关推荐
非专业程序员Ping15 小时前
从0到1自定义文字排版引擎:原理篇
ios·swift·assembly·font
HarderCoder1 天前
【Swift 筑基记】把“结构体”与“类”掰开揉碎——从值类型与引用类型说起
swift
HarderCoder1 天前
Swift 字符串与字符完全导读(三):比较、正则、性能与跨平台实战
swift
HarderCoder1 天前
Swift 字符串与字符完全导读(一):从字面量到 Unicode 的实战之旅
swift
HarderCoder1 天前
Swift 字符串与字符完全导读(二):Unicode 视图、索引系统与内存陷阱
swift
非专业程序员Ping2 天前
一文读懂字体文件
ios·swift·assembly·font
wahkim2 天前
移动端开发工具集锦
flutter·ios·android studio·swift
非专业程序员Ping2 天前
一文读懂字符、字形、字体
ios·swift·font
东坡肘子2 天前
去 Apple Store 修手机 | 肘子的 Swift 周报 #0107
swiftui·swift·apple