Swift4.2 枚举

枚举 为一组相互关联的值定义一种通用类型,它能确保我们在代码中类型安全地使用这些值。


假如你熟悉 C 语言,你应该知道 C 语言中的枚举会为一系列整形值分配相关联的名称。Swift 中的枚举更加灵活,我们不必给每个 case 都提供一个值。如果给枚举的每个 case 分配一个值(被称作“原始”值),这个值可以是字符串或字符类型,也可以是整形或者浮点数。

此外,枚举中的 case 能够被指定为 任意 不同类型的关联值,类似于其它语言中的关联体(unions)和变形体(variants)。我们用枚举定义了一系列相关的情形值时,每个情况的关联值都可以是不同类型。

枚举在 Swift 中是一等类型,它采用了许多传统语言中只能被类支持的特性,例如计算属性(用来提供枚举值的附加信息)和实例方法(用来提供与枚举值相关联的一些方法)。枚举也可以定义构造函数,用来提供初始值;可以在原有实现的基础上进行功能上的扩展;还可以通过遵守协议来提供标准功能。

枚举语法

使用 enum 关键词来声明枚举并把它们的全部定义放在一对大括号内:

1
2
3
enum SomeEnumeration {
// 枚举的定义放在这里
}

下面的例子是使用枚举表示指南针的四个方向:

1
2
3
4
5
6
enum CompassPoint {
case north
case south
case east
case west
}

枚举中定义的值 (比如 north, south, east, 和 west) 是枚举的成员。使用 case关键字来定义一个新的枚举成员。

注意
不像 CObjective-CSwift 的枚举成员在创建时不会被赋予一个默认的整型值。在上述CompassPoint 一例中,north, south, eastwest 不会被隐式赋值为 0, 1, 23。相反,这些不同的枚举成员本身就是完备的值,并且是明确定义的CompassPoint类型。

多个成员可以出现在同一行,并以逗号来隔开:

1
2
3
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

每个枚举都定义了一个全新的类型。像 Swift 中其他的类型一样,它们的名字(例如 CompassPointPlanet )应该以大写字母开头。为了阅读起来更简洁明了,以单数形式命名枚举,而不是复数形式:

1
var directionToHead = CompassPoint.west

directionToHead 的类型在被 CompassPoint 的一个值初始化时可以推断出来。一旦 directionToHead 被声明为 CompassPoint类型,你可以使用更简短的点语法来设置其为一个不同的 CompassPoint 值:

1
directionToHead = .east

因为 directionToHead 的类型已知,所以你可以在赋值阶段省略类型名。在使用显示类型的枚举值时,这种写法让代码具有更高的可读性。

用 Switch 语句来匹配枚举值

switch 语句中你可以对每个枚举值进行匹配:

1
2
3
4
5
6
7
8
9
10
11
12
directionToHead = .south
switch directionToHead {
case .north:
print("Lots of planets have a north")
case .south:
print("Watch out for penguins")
case .east:
print("Where the sun rises")
case .west:
print("Where the skies are blue")
}
// 打印 "Watch out for penguins"

这段代码可以理解为:

「就 directionToHead 这个值而言,当它等于 .north 时,输出 Lots of planets have a north,当它是 .south 时则会输出 Watch out for penguins」。

以此类推。
在 控制流 中提到, 一个 switch 语句必须列举出枚举中的所有值。 如果漏掉了 .west 这个 case,这段代码就会因为没有考虑到 CompassPoint 这个枚举中的所有情况而不会被编译。 需要穷举所有的情况来确保不会有遗漏。

当不需要给每一个枚举中的情况都写一个 case 时,你可以用 default 来代替其他所有没有被声明的情况:

1
2
3
4
5
6
7
8
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// 打印 "Mostly harmless"

遍历枚举的情况

定义一个所有枚举情况的集合在某些枚举中是很有用的。你通过在枚举的名字后面写 : CaseIterable 来使用它。SwiftallCases 这个属性来暴露出这个枚举中所有 case 的集合。例子如下:

1
2
3
4
5
6
enum Beverage: CaseIterable {
case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available")
// 打印 "3 beverages available"

在上面的例子中,通过 Beverage.allCases 可以得到 Beverage 这个枚举包含的所有 case 的集合。你可以想其他集合一样使用 allCases – 集合中的元素为这个枚举的值,在这个例子中也就是 Beverage 的值。上面的例子中得到了枚举中 case 的个数,而下面的例子中则用 for 循环遍历了所有的 case

1
2
3
4
5
6
for beverage in Beverage.allCases {
print(beverage)
}
// coffee
// tea
// juice

关联值

上一节的示例演示了枚举本身是如何被定义(和分类)的。你可以为 Planet.earth 设置一个常量或变量,并在稍后查看此值。但是,如果能够在这些成员值旁边存储其他类型的 关联值 就更方便了。这能让你存储成员值之外的其他自定义信息,并且每次在代码中使用该成员值时允许这个信息发生变化。

你可以定义 Swift 枚举以存储任何给定类型的关联值,并且如果需要,每种枚举的值类型可以不同。这种枚举在其他编程语言中称为 区分联合 , 标记的联合 或 变体。

例如,假设库存跟踪系统需要通过两种不同类型的条形码跟踪产品。有些产品使用标有数字 09UPC 格式的 1D 条形码。每个条形码都有一个「数字系统」码,后面跟着五个「制造商代码」码和五个「产品代码」码。然后是「检查」码,以验证条形码是否被正确扫描:

其他产品使用 QR 格式的二维条码进行标记,可以使用任何ISO 8859-1字符,并且可以编码长达 2953 个字符的字符串:

对于库存跟踪系统来说,如果能将 UPC 条形码存储为四个整数的元组,同时将 QR 条形码存储为任意长度的字符串,会是最方便的。

在 Swift 中,定义这两种类型的产品条形码的枚举如下所示:

1
2
3
4
enum Barcode{
case upc(Int, Int, Int, Int)
case qrCode(String)
}

这可以理解为:
「定义一个名为 Barcode 的枚举类型,它可以是一个 upc 的值,带有( IntIntIntInt )类型的关联值,或者是一个 qrCode的值,带有 String 类型的关联值。」

这个定义没有提供任何实际的 IntString 值 — 它只定义了,当一个 Barcode 类型的常量或变量在等于 Barcode.upcBarcode.qrCode 时,可以存储的相关值的 类型 。

可以使用以下任一类型创建新的条形码:

1
var productBarcode = Barcode.upc(8, 85909, 51226, 3)

这个例子创建了一个名为 productBarcode 的新变量,并为它赋值 Barcode.upc ,其关联值为 (8, 85909, 51226, 3)

可以为同一产品分配不同类型的条形码:

1
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

这时,原始的 Barcode.upc 和其整形数值被新的 Barcode.qrCode 和其字符串值所替代。条形码的常量和变量可以存储为一个 .upc 或者一个 .qrCode (连同它的关联值),但是在任何指定时间只能存储其中之一。

像以前一样,不同的条形码可以使用一个 switch 语句来检查。然而这次关联值可以被提取作为 switch 语句的一部分。你可以在 switchcase 分支代码中提取每个关联值作为一个常量(用 let 前缀)或者一个变量(用 var 前缀)来使用:

1
2
3
4
5
6
7
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
print("QR code: \(productCode).")
}
// 打印 "QR code: ABCDEFGHIJKLMNOP."

如果一个枚举成员的所有关联值都被提取作为常量,或者全被提取作为变量,为了简洁,你可以放置一个 var 或者 let 标注在成员名称的前面:

1
2
3
4
5
6
7
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
// 打印 "QR code: ABCDEFGHIJKLMNOP."

原始值

在 关联值 小节的条形码例子中演示了一个枚举成员如何声明它们存储着不同类型的关联值。作为关联值的替代,枚举成员可以被默认值(称为 原始值 )预先填充,其中这些原始值具有相同的类型。

1
2
3
4
5
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}

在这里,称为 ASCIIControlCharacter 的枚举的原始值被定义为字符型 Character ,并被设置了一些比较常见的 ASCII 控制字符。字符 值的描述请详见 字符串和字符。

原始值可以是字符串,字符,或者任意整型值或浮点型值。每个原始值在枚举声明中必须是唯一的。

注意
原始值和关联值是 不相同 的。当你开始在你的代码中定义枚举的时候原始值是被预先填充的值,向上述的三个 ASCII 值。对于一个特定的枚举成员,它的原始值始终是相同的。关联值是你在创建一个基于枚举成员的新常量或者变量时才会被设置的,并且每次当你这么做的时候,它的值可以不同。

原始值的隐形赋值

在使用原始值为整型值或者字符串类型的枚举时,不需要显式的给每一个枚举成员设置原始值,Swift 会自动赋值。

例如,如果使用整型值作为原始值,隐式赋值的值会依次递增1.如果第一个枚举成员没有设置原始值,那么它的原始值就是 0

下面的枚举是对之前的 Planet 这个枚举的一个细化,利用原始整型值来代表每个 planet 在太阳系中的顺序:

1
2
3
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

在上述例子中, Planet.mercury 有一个显式值 1 , Planet.venus 有一个隐式值 2 , 依次类推。

当使用字符串作为原始值时,每个枚举成员的隐式初值是该成员的名称。

下面的枚举是对之前 CompassPoint 枚举的改进,其中使用字符串原始值表示每个方向的名称:

1
2
3
enum CompassPoint: String {
case north, south, east, west
}

在上面的例子中, CompassPoint.south 有一个隐含的原始值 「south」 ,依此类推。

你可以使用其 rawValue 属性访问枚举的原始值:

1
2
3
4
5
let earthsOrder = Planet.earth.rawValue
// earthsOrder 的值为 3

let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection 的值为 "west"

使用原始值初始化

如果使用原始值类型定义枚举,该枚举会自动获得一个初始化方法,该初始化方法接受原始值类型的值(作为名为 rawValue 的参数)并返回枚举成员或 nil 。你可以使用此初始化方法尝试创建枚举的新实例。

这个例子从原始值 7 中识别出天王星:

1
2
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet 是 Planet? 类型,并且等于 Planet.uranus

然而,并非所有 Int 值都会找到匹配的行星。因此,原始值初始化方法始终返回 可选 枚举成员。在上面的例子中, possiblePlanet 的类型是 Planet? ,或「可选的 Planet 」

注意
原始值构造器是一个可失败构造器,因为并非每个原始值都能返回对应的枚举成员。有关更多信息,请参阅 可失败构造器

如果你试图找到一个位置为 11 的行星,那么原始值初始化方法返回的可选 Planet 值将为 nil

1
2
3
4
5
6
7
8
9
10
11
12
let positionToFind = 11
if let somePlanet = Planet(rawValue: positionToFind) {
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
} else {
print("There isn't a planet at position \(positionToFind)")
}
// 打印 "There isn't a planet at position 11"

此示例使用可选绑定来尝试访问原始值为 11 的行星。 语句 if let somePlanet = Planet(rawValue: 11) 创建了一个可选的 Planet ,并且当可选 Planet 有返回值时,将 Planet 的值赋给 somePlanet 。 在这个例子中,不可能找到位置为 11 的行星,因此执行 else 分支。

递归枚举

递归枚举 是枚举的一种,它允许将该枚举的其他实例,作为自己一个或多个枚举成员的关联值。 你可以通过在枚举成员之前加上 indirect 来表示枚举成员是递归的,它将告诉编译器插入必要的间接层。

例如,这是一个存储简单算术表达式的枚举:

1
2
3
4
5
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

你还可以在枚举的开头加入 indirect ,以将所有具有关联值的枚举成员标示为可递归的:

1
2
3
4
5
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}

此枚举可以存储三种算术表达式:普通数字、两个表达式的相加以及两个表达式的相乘。 additionmultiplication 枚举成员的相关值同时也是算术表达式 — 这使得嵌套表达式成为可能。 例如,表达式 (5 + 4) * 2 在乘法的右侧有一个数字,在乘法的左侧有另一个表达式。 因为数据是嵌套的,用于存储数据的枚举也需要支持嵌套 — 这意味着枚举需要是可递归的。 下面的代码展示了为 (5 + 4) * 2 创建的 ArithmeticExpression 递归枚举:

1
2
3
4
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

递归函数是一种处理递归结构数据的简单方法。 例如,这是一个计算算术表达式的函数:

1
2
3
4
5
6
7
8
9
10
11
12
func evaluate(_ expression: ArithmeticExpression) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}

print(evaluate(product))

当此函数遇到纯数字,直接返回相关值即可。 当此函数遇到加法或乘法,则分别计算符号左侧和右侧的表达式,然后将它们相加或相乘。