Swift 枚举
枚举(Enumeration)用来描述「一组有限的可能取值」。
比如一周只有七天、订单只有几种状态,用枚举比用字符串常量安全得多:写错的 case 会在编译期被发现。
Swift 的枚举比 C 语言强大,可以带关联值、方法、计算属性,还能遵循协议。
枚举是值类型,赋值和传参时按值复制,这点和结构体一样。
定义与 case
用 enum 关键字定义枚举,每个取值用 case 列出。
多个 case 可以写在同一行,用逗号分隔。
实例
// 定义一周中的工作日
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 这类开放类型必须写。
实例
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 可以混在同一个枚举里。
它很适合描述「操作结果」这类形态不固定的数据。
实例
// 关联值:每个 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:) 构造器。
实例
// 原始值:同一个枚举里所有 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 静态属性,类型是数组,顺序和声明顺序一致。
实例
// 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 让这一层通过间接存储来实现。
实例
// 递归枚举: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 就是为这种场景准备的兜底分支。
实例
// @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 的声明顺序,这一点和字典不同,可以放心依赖。
