枚举(Enumerations)

436 阅读12分钟

枚举为一组相关的值定义了一个常用的类型并且让你可以在代码中类型安全的使用那些值。

如果你对C熟悉,你会知道C枚举对一个序列的整型值分配相关的名字。swift中的枚举更灵活,不需要为每个枚举的case提供一个值。如果为每个枚举的case提供一个值(就是raw value),这个值可以使string,chararchter,或者任何整型、浮点型的值。

还有,枚举cases可以指定任何类型的关联的值来和每个不同case的值存在一起,像其他语言的中做法的结合和编译。你可以定义一个普通的相关case的序列作为一个枚举的一部分,每一个有一个不同的和他相关的合适类型的值的序列。

swift中的枚举在他们自己等级中是first-class类型。他们采用了很多之前只被类支持的特性,例如提供额外的关于枚举当前值的计算书型,提供枚举代表的值的相关功能的实例方法。枚举也可以定义初始化方法来提供初始化case值;可以被扩展来增加他们原来实现之外的功能;可以遵守协议来提供标准的功能。

更多这些功能的信息,查看Properties, Methods, Initialization, Extensions, 和 Protocols

枚举语法(Enumeration Syntax)

使用关键字enum介绍枚举将他们全部的定义放在一堆大括号中:

enum SomeEnumeration {
    // enumeration definition goes here
}

这里是关于四个主要的指南针的点的例子:

enum CompassPoint {
    case north
    case south
    case east
    case west
}

定义在枚举中的值(例如north,south,east和west)是它自己的enumeration cases。使用case关键字说明新enumeration cases。

swift枚举的实例没有默认设置的整型值,不像语言C和Objective-C。在上面CompassPoint例子中,north,south,east和west没有隐式等于0,1,2,和3,在他们自己的等级中不同的枚举实例是值,有隐式的的CompassPoint类型。

多个cases可以出现在一行中,使用逗号分隔:

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

每个枚举的定义定义了一个新的类型。像swift中其他的类型,他们的名字以大写字母开始(例如CompassPoint和Planet)。给枚举类型单数而不是双数,所以他们读起来是含义明白的:

var directionToHead = CompassPoint.west

当他用CompassPoint的一个可能值来初始化时directionToHead的类型可以推导出来。一旦directionToHead声明为CompassPoint,你可以使用简短的点语法把它设置成不同的CompassPoint值:

directionToHead = .east

directionToHead的类型已经知道了,所以你可以在设置它的值的时候丢掉类型。当使用明确类型的枚举值时这代码可读性更高。

使用switch语句匹配枚举值(Matching EnumerationValues with a Switch Staement)

你可以使用一个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的case中,打印”Lots of planets have a north“。他等于.south的case中,打印”Watch out for penguins“。”

。。。等等。

Control Flow中描述的,当查看一个枚举的cases时一个switch语句必需是详尽的。如果.west的case忽略了,这段代码就不会编译,因为他没有将CompassPoint的实例全部考虑到。详尽的需要可以保证枚举的cases不会偶然的忽略。

当为每一个枚举case提供一个case不合适的时候,你可以提供一个default实例来覆盖没有明确说明的全部cases:

let somePlanet = Planet.earth
switch somePlanet {
case .earth:
    print("Mostly harmless")
default:
    print("Not a safe place for humans")
}
// Prints "Mostly harmless"

遍历枚举Cases(Iterating over Enumeration Cases)

对于一些枚举来说,有一个包含全部枚举的cases的序列很有用。你可以通过在枚举名称后写CaseIterable来做到这一点。swift展示了一个全部cases的序列作为枚举类型的allCases属性。这类是一个例子:

enum Beverage: CaseIterable {
    case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available")
// Prints "3 beverages available"

在上面的例子中,写Beverage。allCases来访问包含Beverage枚举全部cases的序列。你可以像其他序列一样使用allCases--序列的元素时枚举类型的实例,所以在这里他们是Beverage的值。上面的例子计算了有多少个cases,下面的例子使用一个for循环来遍历全部的cases。

for beverage in Beverage.allCases {
    print(beverage)
}
// coffee
// tea
// juice

上面例子中使用的语法将枚举标记为遵守CaseIterable协议。关于协议的信息,查看Protocols

相关值(Associated Values)

在之前章节中的例子展示了枚举的cases在他们自己的等级中如何成为定义的(有类型)的值。你可以将一个常量或者变量设置成Planet。earth,过后查看这个值。不过,有时候能和那些case值一起存储其他类型的值很有用。这个附加的信息称为关联值,在你的代码中每次作为一个值使用那个case时它会变。

你可以定义swift枚举来存储任何给定类型的关联值,如果有需要枚举每个case的值的类型可以使不同的。枚举的相似的这些在其他编程语言中称为discriminated unions,tagged unions,或者variants。

例如,支持一个库存跟踪系统需要通过两种不同的条形码类型追踪产品。在UPC格式中一些产品使用1D条形码标记,使用数字0-9。每个条形码有一个数字,跟在五位厂商编码数字和五位产品编码后面。这些后面跟着一个检查数字来验证码已经被正确的扫描了:


其他商品用二维码的格式的2D条形码来标记,可以使用全部ISO8859-1字符并且可以表一长达2953字符的字符串:


将UPC条形码作为四个整型的元祖,任何长度的二维码作为字符串存储起来,对于库存追踪系统来说非常方便。

swift中,一个定义产品任何类型的条形码的枚举看起来像下面这样:

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

可以理解为:

“定义一个名为Barcode的枚举,可以使用有(Int,Int,Int,Int)类型的关联值的upc的值或者有String类型关联值的qrCode的值。“

这个定义没有提供任何实际的Int或者String值--只是定义了当他们等于Barcode.upc或者Barcode.qrCode时Barcode常量和比那辆可以存储的关联值的类型。

你可以使用两个类型之一创建一个新的条形码:

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

这个例子创建了一个新的名为productBarcode的变量并且分配给他一个关联元祖值为(8,85909,51226,3)的Baracode.upc的值。

你可以给相同的产品设置一个barcode的不同类型:

productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

这里,原来的Barcode。upc和它的整型值被新的Barcode。qrCode和它的string值代替了。Barcode类型的常量和变量可以存储a.upc或者a.qrCode(和他们关联值一起),但是他们在任何时候他们只能存储他们中的一个。

你可以使用switch语句检查不同的barcode类型,像Matching Enumeration Values with a Switch Statement中的例子。这里,不过,关联值作为switch语句的一部分提取出来。为了在switch的case的主体中使用你将每个关联值作为一个常量(使用let前缀)或者变量(使用var前缀)提取出来:

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."

原始值(Raw Values)

Associated Values中的条形码例子展示了枚举的cases如何声明他们存储不同类型的关联值。作为关联值的另一种方式,枚举cases可以用默认值(名为raw values)预先填充,全都是相同类型的。

这里是一个和命名的枚举cases一起存储原始ASCII值的例子:

enum ASCIIControlCharacter: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}

这里,名为ASCIIControlCharacter的枚举的原始值定义为了Character类型,并设置成了一些很普通的ASCII控制字符。Character值描述在Strings and Characters

原始值可以使字符串,字符,或者任何整型或者浮点类数字类型。在他自己的枚举生命中每个raw vale必须是独一无二的。

原始值和关联值不一样。当你在代码中第一次定义枚举时原始值被设置成预先填充的值,像上面三个ASCII代码。特定枚举case的原始值通常是一样的。关联值在你以枚举cases之一为基础创建一个新的常量或者变量的时候设置,每次你这么做的时候可以不一样。

隐式分配原始值(Implicitly Assigned Raw Values)

当你使用一个存储整型或者字符串值的枚举时,不需要明确的给他们每个case分配原始值。你没做的时候,swift自动为你分配值。

例如,当为原始值使用整型时,每个case的默认值比前一个case多一,如果第一个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“,等等。

你使用它的rawValue属性来方位枚举case的原始值:

let earthsOrder = Planet.earth.rawValue
// earthsOrder is 3

let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection is "west"

初始化原始值(Initializing from a Raw Value)

如果用一个原始值类型定义了一个枚举,枚举自动接受一个初始化方法,接受一个原始值类型的值(参数名为rawValue)并返回了一个枚举case或者nil。你可以使用这个枚举来创建一个枚举的新实例。

这个例子用它的原始值7确认Uranus:

let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet is of type Planet? and equals Planet.uranus

不是全部Int值都会找到匹配的行星,不过。因为如此,原始值初始化方法通常返回一个可选枚举case。在上面的例子中,possiblePlanet的类型是Planet?,或者”optional Planet“。

原始值初始化方法是一个可能出错的初始化方法,因为不是每一个原始值都会返回一个枚举case。更多的信息,查看Failable Initializers

如果你尝试用位置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,如果可以获取到将somePlanet设置为那个可选Planet的值。这种情况下,不可能使用位置11获取到行星,所以else分支代替执行。

递归枚举(Recursive Enumerations)

一个递归枚举是一个有另外一个枚举实例作为一个或者多个枚举case关联值的枚举。在它前面写indirect来标明枚举case是一个递归枚举,告诉编译器插入需要的间接层。

例如,这里是一个存储一个数学表达式的枚举:

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

你也可以在枚举的开头写indirect来使全部有关联值的枚举的cases是间接的:

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

这个枚举可以存储三种数学表达式:一个简单的数字,两个表达式相加,两个表达式相乘。addition和multiplication的cases有相关联的也是数学表达式的值--这些关联值使内嵌表达式成为可能。例如,表达式(5+4)*2在乘法右边有一个数并且乘法左边有另一个表达式。因为数据是内嵌的,用来存储数据的枚举也需要支持内嵌--这意味着美剧需要时递归的。下面的代码展示了ArithmeticExpression递归枚举为(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"

这个函数通过只返回关联函数处理简单的数字。通过处理左边的表达式,处理右边的表达式来处理addition或者multiplication,然后将他们相加或者相乘。