现在位置: 首页 > Swift 教程 > 正文

Swift 枚举

枚举(Enumeration)用来描述「一组有限的可能取值」。

比如一周只有七天、订单只有几种状态,用枚举比用字符串常量安全得多:写错的 case 会在编译期被发现。

Swift 的枚举比 C 语言强大,可以带关联值、方法、计算属性,还能遵循协议。

枚举是值类型,赋值和传参时按值复制,这点和结构体一样。


定义与 case

enum 关键字定义枚举,每个取值用 case 列出。

多个 case 可以写在同一行,用逗号分隔。

实例

import Foundation

// 定义一周中的工作日
enum RunoobWeekday {
    case monday, tuesday, wednesday, thursday, friday
}

func plan(_ day: RunoobWeekday) -> String {
    switch day {
    case .monday:
        return "周一:例会"
    case .tuesday, .wednesday, .thursday:
        return "周中:开发"
    case .friday:
        return "周五:发布"
    }
}

print(plan(.wednesday))
print(plan(.friday))

运行结果:

周中:开发
周五:发布

引用枚举值时有两种写法:完整写法是 RunoobWeekday.wednesday,在类型已知的上下文里可以简写成 .wednesday

上面调用 plan(.wednesday) 时,参数类型已经确定,所以省略了枚举名。


与 switch 配合

枚举最常见的用法就是配合 switch,这也是它比字符串常量安全的地方。

switch 会检查是否覆盖了所有 case,漏掉一个就是编译错误,不需要写 default。

反过来说,只有当 switch 已经穷尽(枚举全部列出、或 Bool 的两种情况)时才可以省略 default,对 Int 这类开放类型必须写。

实例

import Foundation

enum RunoobGrade: Int, CaseIterable {
    case a = 90, b = 80, c = 60

    // 枚举可以带计算属性
    var isPass: Bool { rawValue >= 60 }

    // 枚举也可以带方法
    func label() -> String {
        switch self {
        case .a: return "优秀"
        case .b: return "良好"
        case .c: return "及格"
        }
    }
}

// 配合 where 过滤
for grade in RunoobGrade.allCases where grade.rawValue >= 80 {
    print("\(grade.rawValue) -> \(grade.label())")
}
print(RunoobGrade.c.isPass)

运行结果:

90 -> 优秀
80 -> 良好
true

枚举内部用 self 表示当前这个取值,所以 label() 可以直接对 self 做 switch。


关联值

原始值是「每个 case 固定对应一个值」,关联值则相反:每个 case 可以携带自己的数据,而且不同类型、不同数量的 case 可以混在同一个枚举里。

Swift 枚举的三种形态对比:纯枚举只表示一种取值、带原始值每个 case 对应一个固定值、带关联值每个 case 携带不同类型的数据,并标注 CaseIterable 与 indirect

它很适合描述「操作结果」这类形态不固定的数据。

实例

import Foundation

// 关联值:每个 case 可以携带不同类型的数据
enum RunoobResponse {
    case success(data: String)
    case failure(code: Int, message: String)
    case loading
}

func handle(_ response: RunoobResponse) {
    switch response {
    case .success(let data):
        print("成功:\(data)")
    case .failure(let code, let message):
        print("失败 [\(code)]:\(message)")
    case .loading:
        print("加载中...")
    }
}

handle(.success(data: "来自 www.runoob.com 的响应"))
handle(.failure(code: 404, message: "页面不存在"))
handle(.loading)

运行结果:

成功:来自 www.runoob.com 的响应
失败 [404]:页面不存在
加载中...

取值时用 case .success(let data) 把关联值绑定到局部常量,再在分支里使用。

如果只关心某个关联值,也可以用 where 加条件过滤,或者只绑定一部分。

注意:关联值和原始值不能混用在同一个 case 上。一个 case 要么携带关联值,要么有原始值,不能两者都有。


原始值与 RawRepresentable

原始值(Raw Value)让每个 case 对应一个固定值,类型可以是字符串、字符、整数或浮点数,同一个枚举里必须统一。

带原始值的枚举自动遵循 RawRepresentable 协议,于是就有了 rawValue 属性和一个可失败的 init?(rawValue:) 构造器。

实例

import Foundation

// 原始值:同一个枚举里所有 case 的原始值必须是同一类型
enum RunoobStatus: Int {
    case created = 1
    case processing
    case finished
}

enum RunoobSite: String {
    case home = "www.runoob.com"
    case name = "RUNOOB"
}

let status = RunoobStatus.processing
print("rawValue = \(status.rawValue)")          // 未写原始值时按上一个值递增

// 通过原始值反查 case,返回的是可选值
if let finished = RunoobStatus(rawValue: 3) {
    print("找到 case:\(finished)")
}
if let unknown = RunoobStatus(rawValue: 99) {
    print(unknown)
} else {
    print("99 没有对应的 case")
}

print(RunoobSite.home.rawValue)

运行结果:

rawValue = 2
找到 case:finished
99 没有对应的 case
www.runoob.com

整数原始值只写第一个,后面的会自动递增,所以 processing 的 rawValue 是 2。

字符串原始值不写时默认就是 case 名本身,这里显式指定成了网址。

反查用的 RunoobStatus(rawValue:) 返回的是可选值,找不到就返回 nil,因此要用 if let 或空合运算符处理。


CaseIterable 与 allCases

想让枚举的所有 case 变成一个可以遍历的集合,让它遵循 CaseIterable 协议即可。

编译器会自动合成一个 allCases 静态属性,类型是数组,顺序和声明顺序一致。

实例

import Foundation

// CaseIterable:自动合成 allCases,顺序与声明顺序一致
enum RunoobPlanet: String, CaseIterable {
    case mercury = "水星"
    case venus = "金星"
    case earth = "地球"
}

print(RunoobPlanet.allCases.count)
for planet in RunoobPlanet.allCases {
    print("\(planet.rawValue) -> \(planet)")
}

运行结果:

3
水星 -> mercury
金星 -> venus
地球 -> earth

注意直接打印 planet 得到的是 case 名 mercury,不是原始值,要拿原始值得写 planet.rawValue。

allCases 常见于「把所有选项渲染成一个列表」的场景,比如设置页的下拉选项。

注意:带关联值的 case 不能自动合成 allCases,因为编译器无法为它生成一个统一的数组元素。这种情况下需要手写 allCases。


递归枚举 indirect

有些数据结构天生是递归的,比如表达式树:一个加法节点里又包含两个表达式。

如果直接让枚举的 case 持有自身,编译器无法确定它的大小,必须加 indirect 让这一层通过间接存储来实现。

实例

import Foundation

// 递归枚举:case 内部又用到自身,必须用 indirect 打破无限大小
indirect enum RunoobExpression {
    case number(Int)
    case add(RunoobExpression, RunoobExpression)
    case multiply(RunoobExpression, RunoobExpression)
}

func evaluate(_ expression: RunoobExpression) -> Int {
    switch expression {
    case .number(let value):
        return value
    case .add(let left, let right):
        return evaluate(left) + evaluate(right)
    case .multiply(let left, let right):
        return evaluate(left) * evaluate(right)
    }
}

// 表示 (1 + 2) * 3
let expression = RunoobExpression.multiply(
    .add(.number(1), .number(2)),
    .number(3)
)
print(evaluate(expression))

运行结果:

9

indirect 写在 enum 前面表示整个枚举的 case 都允许递归;也可以只写在某个 case 前面,只让那个 case 变成间接存储。

计算时用递归函数遍历,遇到 .number 返回数值,遇到运算符就递归求值再合并。


@unknown default

还有一种情况:枚举不是自己定义的,而是来自系统框架或其他模块。

这类枚举将来可能在框架升级时新增 case,如果 switch 里写死了当前所有 case,新版本一编译就会失败。

@unknown default 就是为这种场景准备的兜底分支。

实例

import Foundation

// @unknown default:用于其他模块(如系统框架)定义的枚举,兜底未来新增的 case
func describe(_ result: ComparisonResult) -> String {
    switch result {
    case .orderedSame:
        return "完全相同"
    case .orderedAscending:
        return "前者更小"
    case .orderedDescending:
        return "前者更大"
    @unknown default:
        return "未来新增的情况"
    }
}

print(describe("runoob".compare("RUNOOB")))
print(describe("abc".compare("abd")))

运行结果:

前者更大
前者更小

字符串比较用的是 Unicode 顺序,小写字母排在对应的大写字母之后,所以 "runoob" 比 "RUNOOB" 大。

注意:@unknown default 只能放在 switch 的最后,而且只对「外部模块定义、可能新增 case」的枚举有意义。对自己定义的枚举用它,编译器会提示 switch 还不完整。


常见问题

下面整理了枚举使用中常见的几个疑问。

枚举可以有构造器吗?

可以。枚举可以有指定构造器和便利构造器,也可以用带原始值的 init?(rawValue:)

枚举是值类型,构造器里给自身赋值同样要保证所有存储属性都有初值。

枚举能遵循协议吗?

可以,而且很常用。遵循 CaseIterable 得到 allCases,遵循 Codable 可以直接编解码。

带原始值的枚举遵循 Codable 时,编码结果就是原始值。

原始值和关联值到底怎么选?

需要和外部系统交换一个固定值时用原始值,比如接口返回的状态码。

需要携带结构化数据、且每个 case 携带的内容不一样时用关联值,比如网络请求的成功与失败。

枚举的比较用什么?

不带关联值的枚举可以直接用 == 比较,编译器会自动合成相等性。

带关联值的枚举需要让它遵循 Equatable,只要关联值本身可比较,也能自动合成。

allCases 的顺序有保证吗?

有,顺序就是 case 的声明顺序,这一点和字典不同,可以放心依赖。