枚举(Enumerations)
枚举(Enumerations)为一组相互关联的数值定义了一个通用类型,并确保了这些数值的使用是类型安全(类型安全的语言可以让你清楚地知道代码要处理的值的类型)的。
如果你熟悉C语言,就该知道C语言的枚举是为一组整数赋值与数据相关的名称。Swift中的枚举要灵活的多,且不必为枚举中的每个元素提供一个数值。如果非要给枚举中的元素提供数值(即原始值)的话,那么该值可以是字符串,字符,或者任意整形和浮点型数值。
另外,枚举元素还可以指定任意类型的关联值,这与其他语言中的Union
和Variant
数据类型相似。可以为枚举定义一组相互关联的枚举元素,每个元素都可以设置一组恰当类型的关联值。
Swift中,枚举是一等数据类型。枚举拥有了很多一般只有类才会支持的特性,比如为枚举当前值提供额外信息的计算属性(computed properties),以及与枚举值相关的功能的实例方法(instance methods)。枚举还能定义构造函数(initializers)以提供初始值;能在其原始实现的基础上扩展方法;遵守协议以提供标准化的功能。
有关枚举相关功能的更多信息,请参见Properties
, Methods
, Initialization
, Extensions
以及Protocols
。
枚举语法
使用enum
关键字声明枚举,并将枚举的定义写在一对大括号内:
enum SomeEnumeration {
// enumeration definition goes here
}
下面是指南针四个方向的枚举案例:
enum CompassPoint {
case north
case south
case east
case west
}
定义在枚举中的值(如north
, south
, east
, west
)称为枚举元素(enumeration cases)。使用case
关键字声明新的枚举元素。
注意
与C和OC不同,Swift的枚举元素在创建时并不会被赋值一个默认整数值。在上面的CompassPoint
例子中,north
,south
,east
,west
并不等价于0,1,2,3。相反,每个枚举元素都可以设置不同的值。
多个枚举元素可以写在一行,用逗号隔开:
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
每个枚举定义都定义了一个新类型。跟Swift中其他类型一样,枚举类型的名称(如CompassPoint
和Planet
)也应首字母大写。枚举命名时尽量使用单数而非复数名称,这样阅读起来更一目了然:
var directionToHead = CompassPoint.west
变量directionToHead
的类型在被初始化为CompassPoint
的枚举元素值时由系统推断。一旦变量被声明为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
”。
…以此类推。
根据控制流(Control Flow)的描述, 在判断枚举元素时,switch
语句要穷尽枚举的所有元素。如果.west
这个枚举元素被省略,这段代码就无法编译,因为没有穷尽枚举CompassPoint
的所有元素列表。穷尽所有元素确保了枚举元素不会被遗漏。
在switch
语句中,当为每个枚举元素都提供一个条件判断(case
)显得并不恰当时,可以使用default case
代替那些没有明确表达的枚举元素:
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// Prints "Mostly harmless"
遍历枚举元素
对于一些枚举来说,收集枚举的所有元素是有用的。在枚举名称之后添加: CaseIterable
,Swift就会将该枚举的全部元素放到一个集合中,作为该枚举类型下名为allCases
的属性。例子如下:
enum Beverage: CaseIterable {
case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available")
// Prints "3 beverages available"
在上面例子中,通过Beverage.allCases
访问保存有枚举Beverage
全部元素的集合。集合all Cases
的使用与其他集合一样——集合的元素就是枚举类型的实例,在该例子中即为枚举Beverage
的元素值。上面例子计算了Beverage
枚举的元素个数,下面例子则使用for
循环遍历了所有枚举元素。
for beverage in Beverage.allCases {
print(beverage)
}
// coffee
// tea
// juice
以上例子中使用的语法表示该枚举遵守了CaseIterable
协议。有关协议的更多信息,请参见Protocols
。
关联值
上一节中的例子表明枚举的各元素是可以定义具体数值(和类型)的。比如Planet.earth
赋值给一个常量/变量值,之后再查看。但是,除了枚举元素的具体数值以外,有时候让枚举元素存储一些其他类型的关联值(associated values)是有用的。使得除了存储元素值以外,还可以存储一些额外的定制信息,并且允许每次使用该枚举元素时,其对应的关联值都不同。
Swift中的枚举可以定义存储任意类型的关联值,且根据需要枚举各元素的关联值类型可以不同。Swift中的枚举与其他编程语言里的差别联合(discriminated union
),标签联合(agged union
),变体(variant
)类型相似。
例如,假设一库存追踪系统需要通过两种不同类型的编码来追踪商品。部分商品以UPC格式的一维条形码为标签,使用0-9这10个数字表示。每个条形码都有1个表示“数字系统”的数字,紧跟着5个表示“厂商码”的数字和5个表示“商品码“的数字。最后一位是验证该条形码是否正确扫描的”检查“数字:
另一部分商品以QR码格式的二维码为标签。二维码可以使用任意ISO 8859-1字符,且可编码成一串最长为2,953个字符的字符串:
对于库存追踪系统来说,如果能将UPC格式的条形码储存成一个拥有4个整型的 元组( tuple),将QR码格式的二维码储存成任意长度的字符串,就再方便不过了。
在Swift中,两种条形码的枚举可以这样定义:
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
读作:
“定义一个名为Barcode
的枚举,其中元素upc
的关联值类型为(Int, Int, Int, Int
),元素qrCode
的关联值类型为String
”
该定义不提供任何真实的Int
和String
值——只定义了Barcode
枚举类型的常量/变量在被Barcode.upc
和Barcode.qrCode
赋值时所能存储的关联值类型。
条形码变量可以通过以下方式创建:
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
该例子创建了一个名为productBarcode
的变量,并通过Barcode.upc
关联了一个(8, 85909, 51226, 3
)的元组为其赋值。
同一条形码变量还可以使用另一种方式赋值:
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
此时,原始的Barcode.upc
及其关联值被新的Barcode.qrCode
及其关联值所替代。Barcode
枚举类型的常量/变量既可以存储.upc
也可以存储.qrCode
(连同他们的关联值一起),但同一时间只能存储一个。
跟前面一样,不同的编码类型可以使用switch
语句做检查。然而对于关联值来说,可以作为switch
语句的一部分进行提取。在switch
的各条件句中,可以把关联值提取成常量(以let
开头)或变量(以va
r开头):
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."
如果一个枚举元素的所有关联值都以常量提取,或都以变量提取,则简洁起见,可以将单个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."
原始值
关联值小节的条形码例子展示了枚举元素声明其存储不同类型关联值的具体做法。除了可以设置关联值外,枚举元素还可以预先设置相同类型的默认值(叫做原始值(raw value))
下面例子是存储了原始ASCII值的枚举元素:
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
这里,枚举ASCIIControlCharacter
中各元素的原始值被定义为Character
类型,并赋值为ASCII常见的控制字符。Character
值相关内容参见Strings and Characters
。
原始值可以是字符串,字符,或者整型/浮点型数据。在其枚举声明内部,每一原始值都必须是独一无二的。
注意
原始值与关联值并不相同。原始值是在定义枚举之初预先设定给枚举元素的值,正如上面的3个ASCII码所示。对于特定的枚举元素来说,其原始值永远是一样的。当根据枚举元素创建常量/变量时,才会给关联值赋值,且每次的值都可以不一样。
隐式赋值的原始值
对于那些存储了整型或字符串类型原始值的枚举来说,无需为每个枚举元素都显式地赋一个原始值,Swift会自动为你赋值。
例如,当原始值的类型为整型时,每个枚举元素的原始值就是在前一个元素原始值基础上+1。如果枚举的第一个元素没有设定原始值,则其原始值为0。
下面的枚举是对之前Planet
枚举的改良,用整型的原始值代表太阳系中每个星球的顺序:
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
上面例子中,Planet.mercury
的原始值为1
,那么Planet.venus
的原始值就为2
,以此类推。
当原始值是字符串时,每个枚举元素的隐式原始值就是该枚举元素的名称。
下面的枚举是对之前CompassPoint
枚举的改良,用字符串类型的原始值代表每个方向的名称:
enum CompassPoint: String {
case north, south, east, west
}
上面例子中,CompassPoint.south
的隐式原始值是“south
”,以此类推。
通过rawValue
属性访问枚举元素的原始值:
let earthsOrder = Planet.earth.rawValue
// earthsOrder is 3
let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection is "west"
使用原始值初始化枚举实例
如果定义了一个有原始值的枚举,则该枚举会自带一个构造函数,该函数接收一个原始值类型的值(作为构造函数的参数,名为rawValue
)并返回一个枚举元素或直接返回nil
。可以使用该构造函数创建一个新的枚举实例。
下面例子通过枚举的构造函数创建了一个新的枚举实例:
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet is of type Planet? and equals Planet.uranus
但是,并非所有的Int
值都能找到与之对应的行星。正因如此,原始值构造函数永远返回一个可选的(optional
)枚举元素。在上面例子中,possiblePlanet
的类型为Planet?
,即“可选的Planet
类型”
注意
原始值构造函数是可失败的构造函数,因为并非每个原始值都会返回一个枚举元素。更多相关信息,参见可失败构造函数。
如果你要找到位置11
的行星,则原始值构造函数返回的可选Planet
值就为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
的行星。语句if let somePlanet = Planet(rawValue: 11)
创建了一个可选的Planet
,并且如果该可选Planet
有值则赋值给somePlanet
常量。该例中,位置11
的行星并不存在,因此执行了else
分支。
递归枚举
递归枚举(recursive enumeration)是其枚举实例可以作为一个或更多枚举元素的关联值的枚举类型。在枚举元素前加上indirect
来表示其为递归枚举,indirect
告知编译器插入必要的间接层。
例如,下面是一个存储了简单算术表达式的枚举:
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
还可以在枚举类型前加上indirect
,使得所有有关联值的枚举元素都有递归性:
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
该枚举可以存储三种算术表达式:一个素数,两个表达式的和,以及两个表达式的积。addition
和multiplication
枚举元素的关联值还是算术表达式——这些关联值为表达式的嵌套提供了可能。例如,表达式(5 + 4) * 2的乘号右边是一个数字,乘号左边是另一个表达式。由于数据是嵌套的,因此用于存储数据的枚举也需要支持嵌套——也就是说,该枚举需要是递归的。下面代码即表达式(5 + 4) * 2的递归枚举ArithmeticExpression
:
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"
如果表达式是素数,则直接返回该关联值。如果是加法或乘法表达式,则先将运算符左右两边计算出结果以后再将结果相加或想乘。
后记:
基于Swift4.2最新版本官方文档
技术文章同步更新在公众号【算什么扣得儿】,欢迎关注