枚举为一组相关值定义一个公共类型,并使我们能够在代码中以类型安全的方式使用这些值。
如果我们熟悉C,我们将知道C枚举将相关名称分配给一组整数值。Swift中的枚举要灵活得多,不必为枚举的每种情况提供一个值。如果为每个枚举情况提供了一个值(称为原始值),则该值可以是字符串、字符或任何整数或浮点类型的值。
或者,枚举case可以指定任何类型的关联值,这些值将与每个不同的case值一起存储,就像在其他语言中的联合或变体所做的那样。可以将一组公共的相关case定义为一个枚举的一部分,每个枚举都有一组与之关联的适当类型的不同值。
Swift中的枚举本身就是一级类型。它们采用了许多传统上仅由类支持的特性,例如,计算属性提供有关枚举当前值的附加信息,实例方法提供与枚举表示的值相关的功能。枚举还可以定义初始值设定项以提供初始大小写值;可以扩展以将其功能扩展到原始实现之外;并且可以遵守协议以提供标准功能。
有关这些功能的更多信息,请参阅属性、方法、初始化、扩展和协议。
枚举语法
使用enum
关键字引入枚举,并将其整个定义放在一对大括号中:
enum SomeEnumeration {
// enumeration definition goes here
}
以下是罗盘四个主要点的示例:
enum CompassPoint {
case north
case south
case east
case west
}
枚举中定义的值(如north、south、east和west)是其枚举case。我们可以使用case关键字来引入新的枚举案例。
注意
Swift枚举案例在默认情况下不设置整数值,这与C和Objective-C等语言不同。在上面的CompassPoint示例中,north、south、east和west并不隐式地等于0、1、2和3。相反,不同的枚举case本身就是值,具有显式定义的CompassPoint类型。
多个case可以出现在一行中,用逗号分隔:
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
每个枚举定义定义一个新类型。与Swift中的其他类型一样,它们的名称(如CompassPoint
和Planet
)以大写字母开头。为枚举类型指定单数名称而不是复数名称,以便它们的含义不言而喻:
var directionToHead = CompassPoint.west
directionToHead的类型是在用CompassPoint的一个可能值初始化时推断出来的。directionToHead声明为CompassPoint后,可以使用较短的点语法将其设置为其他CompassPoint值:
directionToHead = .east
directionToHead的类型是已知的,因此在设置其值时可以删除该类型。当使用显式类型化的枚举值时,这使得代码具有高度可读性。
将枚举值与Switch语句匹配
可以使用switch语句匹配各个枚举值:
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")
}
// Prints "Watch out for penguins"
我们可以将此代码理解为:
“考虑directionToHead的值。如果它等于.north
,打印“Lots of planets have a north”。如果它等于.south,打印“Watch out for penguins”
……等等。
如控制流中所述,在考虑枚举的情况时,switch语句必须是详尽的。如果省略.west的case,则此代码不会编译成功,因为它没有考虑到CompassPoint case的完整列表。要求穷尽性可以确保枚举案例不会被不小心忽略。
如果不适合为每个枚举case提供一个case,则可以提供一个默认case来涵盖任何未明确解决的案例:
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// Prints "Mostly harmless"
迭代枚举案例
对于某些枚举,收集该枚举的所有case是很有用的。我们可以通过在枚举的名称后写入:CaseIterable来启用它。Swift将所有case的集合公开为枚举类型的allCases
属性。举个例子:
enum Beverage: CaseIterable {
case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available")
// Prints "3 beverages available"
在上面的例子中,我们写Beverage.allCases
访问包含Beverage枚举的所有case的集合。我们可以像使用任何其他集合一样使用allcase
集合的元素,因为它的元素是枚举类型的实例,因此在本例中它们是Beverage值。上面的示例统计有多少个case,下面的示例使用for循环迭代所有case。
for beverage in Beverage.allCases {
print(beverage)
}
// coffee
// tea
// juice
上述示例中使用的语法将枚举标记为遵守CaseIterable
协议。有关协议的信息,请参阅协议。
关联值
上一节中的示例展示了枚举的case本身是如何定义(并命名)值的。可以将常量或变量设置为Planet.earth,稍后检查此值。但是,有时能够将其他类型的值(附加信息)与这些case值一起存储是有用的。这个附加信息称为关联值,每次将case用作代码中的值时,它都会发生变化。
我们可以定义Swift枚举来存储任何给定类型的关联值,如果需要,值类型对于枚举的每个case都可以不同。这些枚举类似于其他编程语言中的有区别的联合、有标记的联合或其变体。
例如,假设库存跟踪系统需要通过两种不同类型的条形码跟踪产品。有些产品用UPC格式的1D条形码进行标记,使用数字0到9。每个条形码有一个数字系统的数字,后跟五个制造商代码数字和五个产品代码数字。后面是一个校验位,用于验证代码是否已正确扫描:
其他产品采用二维条码 QR 码格式进行标记,二维条码可以使用任何ISO 8859-1字符,并且可以对长度不超过2953个字符的字符串进行编码:
对于库存跟踪系统来说,将UPC条码存储为四个整数的元组,将QR条码存储为任意长度的字符串非常方便。
在Swift中,用于定义任一类型产品条形码的枚举可能如下所示:
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
这可以理解为:
定义一个名为Barcode
的枚举类型,它可以采用带有(Int,Int,Int,Int)类型关联值的upc值,也可以采用带有String类型关联值的qrCode值。
这个定义不提供任何实际的Int或String值,它只定义了当Barcode常量和变量等于Barcode.upc或者Barcode.qrCode时,可以存储的关联值的类型。
然后可以使用以下任一类型创建新条形码:
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
本例创建了一个名为productBarcode
的新变量,并为其赋值为关联元组值为(88590951226,3)的Barcode.upc值。
我们可以为同一产品分配不同类型的条形码:
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
此时,原来的Barcode.upc
和其整数值将替换为新的Barcode.qrCode
以及它的字符串值。Barcode
类型的常量和变量可以存储.upc
或.qrCode
(及其关联值),但在任何给定时间只能存储其中一个。
可以使用switch语句检查不同的条形码类型,类似于用switch语句匹配枚举值的case。但是,这一次,关联值被提取为switch语句的一部分。将每个关联值提取为常量(带let前缀)或变量(带var前缀),以便在switch case的主体中使用:
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).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."
如果枚举case的所有关联值都提取为常量,或者所有值都提取为变量,以简洁起见,则可以在case名称前放置单个var或let注释:
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."
原始值
关联值中的条形码示例显示枚举的case如何声明它们存储不同类型的关联值。作为关联值的替代方法,枚举case可以预先填充默认值(称为原始值),这些值都是相同类型的。
下面是一个将原始ASCII值与命名枚举case一起存储的示例:
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
这里,名为ASCIIControlCharacter
的枚举的原始值被定义为Character
类型,并被设置为一些更常见的ASCII控制字符。Character
值在字符串和字符来描述。
原始值可以是字符串、字符或任何整数或浮点数类型。每个原始值在其枚举声明中必须是唯一的。
注意
原始值与关联值不同。当我们第一次在代码中定义枚举时,原始值被设置为预先填充的值,就像上面的三个ASCII代码一样。特定枚举case的原始值始终相同。关联值是基于枚举的一个case创建新的常量或变量时设置的,并且每次创建时都可能不同。
隐式指定的原始值
当使用存储整数或字符串原始值的枚举时,不必为每种情况显式指定原始值。这种情况下如果没有指定原始值,Swift会自动为我们指定值。
例如,当整数用于原始值时,每个case的隐式值都比前一个case加1。如果第一个case没有设置值,则其值为0。
下面的枚举是对早期行Planet
枚举的改进,使用整数原始值来表示每个行星距离太阳的顺序:
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
在上面的例子中,Planet.mercury 显式原始值为1,Planet.venus具有隐式原始值2,依此类推。
当字符串用于原始值时,每个case的隐式值都是该case名称的文本。
下面的枚举是对早期CompassPoint
枚举的改进,使用字符串原始值来表示每个方向的名称:
enum CompassPoint: String {
case north, south, east, west
}
在上面的例子中,CompassPoint.south
具有隐含的原始值“south
”,以此类推。
可以使用枚举case的rawValue属性访问其原始值:
let earthsOrder = Planet.earth.rawValue
// earthsOrder is 3
let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection is "west"
根据原始值初始化
如果定义具有原始值类型的枚举,枚举将自动接收一个初始值设定项,该初始值设置项接受原始值类型的值(作为一个名为rawValue的参数),并返回case或为nil。我们可以使用此初始值设置项尝试创建枚举的新case。
本示例根据其原始值7中创建了天王星:
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet is of type Planet? and equals Planet.uranus
然而,并非所有可能的Int值都能找到匹配的行星。因此,原始值的初始值设定项总是返回可选的枚举case。在上面的例子中,possiblePlanet
是Planet?
,或“可选Planet
”
注意
原始值初始值设定项是一个可能失败的初始值设定项,因为不是每个原始值都会返回枚举case。有关详细信息,请参阅失败的初始值设定项。
如果尝试查找位置为11的行星,则原始值初始值设定项返回的可选行星值将为nil
:
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)")
}
// Prints "There isn't a planet at position 11"
本例使用可选绑定来尝试访问原始值为11的行星。如果let somePlanet = Planet(rawValue: 11)
语句创建一个可选Planet
,并将somePlanet
设置为该可选Planet
的值(如果可以检索)。在本例中,无法检索位置为11的Planet
,因此执行else分支。
递归枚举
递归枚举是将枚举的另一个实例作为一个或多个枚举case的关联值的枚举。我们可以通过在枚举case之前写入indirect
来指示它是递归的,这会告诉编译器插入必要的间接层。
例如,以下是存储简单算术表达式的枚举:
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
也可以在枚举开始之前写入indirect
,以便为具有关联值的枚举的所有case启用间接寻址:
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
此枚举可以存储三种算术表达式:普通数、两个表达式的加法和两个表达式的乘法。addition
和multiplication
都有关联的值,这些值也是算术表达式。这些关联值使嵌套表达式成为可能。例如,表达式(5 + 4) * 2
在乘号的右侧有一个数字,在乘号的左侧有另一个表达式。因为数据是嵌套的,所以用于存储数据的枚举也需要支持嵌套这意味着枚举需要是递归的。下面的代码显示了为(5 + 4) * 2
创建的算术表达式递归枚举:
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))
递归函数是处理具有递归结构的数据的简单方法。例如,下面是一个计算算术表达式的函数:
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))
// Prints "18"
此函数通过简单地返回关联的值来计算普通数。它通过计算左侧的表达式,计算右侧的表达式,然后将它们相加或相乘来计算加法或乘法。