列舉

列舉(enumeration)是可以讓你自定義的一種型別,其內包含一組相關的值(例如表示學校有哪些教學科目或是購買過程中會出現的錯誤狀況)。使你可以在程式碼中以型別安全(type-safe)的方式來使用這些值,且可以更清楚地知道使用同一組列舉型別時,彼此的相互關係。

列舉支援很多特性,例如計算型屬性(computed property)、實體方法(instance method)、定義建構器(initializer)、擴展(extension)及協定(protocol),後面章節會正式介紹這些內容。

列舉語法

列舉使用enum關鍵字建立,並將列舉定義放在一組大括號{}內,格式如下:

enum 列舉的自定義型別 {
    各列舉定義
}

列舉使用case關鍵字定義成員值,例子如下:

//這是一個定義指南針四個方位的列舉
enum CompassPoint {
    case north
    case south
    case east
    case west
}

// 多個成員值可以寫在同一行 以逗號 , 隔開
// 這是一個定義太陽系八大行星的列舉
enum Planet {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

每個列舉都定義了一個全新的型別。像 Swift 中其他型別一樣,列舉的名稱(如上述程式中的CompassPointPlanet)應該以一個大寫字母開頭且為單數。

定義完列舉後,接著將其指派給一個變數,與一般指派方式相同:

// 這邊使用上面定義過的指南針方位的列舉

// 型別為 CompassPoint 的一個變數 值為其列舉內的 west
var directionToHead = CompassPoint.west

// 這時已經可以自動推斷這個變數的型別為 CompassPoint
// 如果要再指派新的值 可以省略列舉的型別名稱
directionToHead = .north

使用 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")
}

相關值

列舉中的每個成員值,視需求可以在需要的時候,一併儲存自定義的一個或以上其他型別相關值(associated value)。使用方法為在成員值後面加上小括號(),並將相關值型別放在小括號內(就像使用元組tuple一樣)。往後在程式中將該列舉成員值指派給變數或常數時,這個(或這些)相關值才會被設置,之後也可以依照需求再修改相關值。

列舉中每個成員所定義的相關值型別可以依照需求設置成不同型別。

以下是一個例子,假設建立一個庫存追蹤系統,商品條碼可能會有UPC格式的一維碼,每一個UPC條碼是一組四個正整數的值,或是使用QR Code格式的二維碼,每一個QR Code條碼是一個最多為 2,953 字元的字串,依據這個條件建立的列舉如下:

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

上述程式的意思是:定義一個名稱為Barcode的列舉型別,有兩個成員值,一個成員值為upc,夾帶著(Int, Int, Int, Int)型別的相關值,另一個成員值為qrCode,夾帶著String型別的相關值。

這個定義沒有提供任何IntString的實際的值,它只是定義了:當一個型別為Barcode的變數或常數被指派為Barcode.upcBarcode.qrCode時,可以儲存的相關值的型別。

這時可以指派一個型別為Barcode的變數,例子如下:

// 指派 Barcode 型別 成員值為 upc
// 相關值為 (8, 85909, 51226, 3)
var productBarcode = Barcode.upc(8, 85909, 51226, 3)

// 如果要修改為儲存 QR Code 條碼
productBarcode = .qrCode("ABCDEFG")

// 這時 .upc(8, 85909, 51226, 3) 會被 .qrCode("ABCDEFG") 所取代
// 一個變數 同一時間只能儲存一個列舉的成員值(及其相關值)

在使用switch語句匹配列舉值時,可以把相關值取出作為常數(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).") // 會印出這行
}

如果相關值全部都被取出作為常數(或變數),可以改成只在成員名稱前加上let(或var),來使程式更為簡潔,如下:

switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
    print("QR Code: \(productCode).")
}

原始值

除了使用相關值的列舉,其內的成員值可以儲存不同型別的相關值。Swift 也提供列舉先設置原始值(raw value)來代替相關值,這些原始值的型別必須相同。使用方法為在列舉名稱後加上冒號:並接著原始值型別,例子如下:

enum WeekDay: Int {
    case monday = 1
    case tuesday = 2
    case wednesday = 3
    case thursday = 4
    case friday = 5
    case saturday = 6
    case sunday = 7
}

let today = WeekDay.friday
// 使用 rawValue 屬性來取得原始值
// 印出:5
print(today.rawValue)
Hint
  • 原始值可以是字串、字元或者任何整數值或浮點數值。
  • 每個原始值在它的列舉宣告中必須是唯一的。

原始值(raw value)跟相關值(associated value)是不同的。原始值在定義列舉時即被設置,對於一個特定的列舉成員,它的原始值始終是相同的。而相關值是在列舉成員被指派為一個變數(或常數)時才一併設置的值,列舉成員的相關值是可以不同的。

原始值的隱式指派

在使用原始值為整數型別的列舉時,可以不需要為每個成員設置原始值,Swift 會將每個成員的原始值依次遞增1,這個特性稱為原始值的隱式指派(implicitly assigned raw value)。成員都沒有原始值時,則會將第一個成員的原始值設置為0,再依序遞增1,例子如下:

// 第一個成員有設置原始值 1, 接著下去成員的原始值就是 2, 3, 4 這樣遞增下去
enum SomePlanet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

let ourPlanet = SomePlanet.earth

// 印出:3
print(ourPlanet.rawValue)

在使用原始值為字串型別的列舉時,可以不需要為每個成員設置原始值,將會直接將該成員值設置為原始值。例子如下:

enum AnotherCompassPoint: String {
    case north, south, east, west
}

let directionPoint = AnotherCompassPoint.east

// 印出:east
print(directionPoint.rawValue)

使用原始值初始化列舉實體

在定義列舉時,如果使用了原始值,則這個列舉會有一個初始化方法(method),這個方法有一個名稱為rawValue的參數,其參數型別就是列舉原始值的型別,返回值為列舉成員或nil。例子如下:

// 一個使用原始值的列舉 原始值依序是 1,2,3,4,5,6,7,8
enum OtherPlanet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

let possiblePlanet = OtherPlanet(rawValue: 7)
// possiblePlanet 型別為 OtherPlanet? 值為 OtherPlanet.uranus

不過不是所有傳入的Int參數都會有返回值,所以其實他是返回一個OtherPlanet?型別,也就是可選的OtherPlanet。以下是一個返回nil的例子:

let positionToFind = 9
if let targetPlanet = OtherPlanet(rawValue: positionToFind) {
    switch targetPlanet {
    case .earth:
        print("We are here !")
    default:
        print("Not Safe !")
    }
} else {
    print("No planet at position \(positionToFind)")
}
// 印出:No planet at position 9

上述程式先使用了一個可選綁定(optional binding),使用原始值9來尋找是否有星球,但可以看到列舉OtherPlanet中沒有原始值為9的成員,所以會返回一個nil,接著則執行else內部程式。

遞迴列舉

遞迴列舉(recursive enumeration)是一種列舉型別,它會有一個或多個列舉成員使用該列舉型別的實體作為相關值。如果要表示一個列舉成員可以遞迴,必須在成員前面加上indirect,例子如下:

// 定義一個列舉
enum ArithmeticExpression {
    // 一個純數字成員
    case number(Int)

    // 兩個成員 表示為加法及乘法運算 各自有兩個[列舉的實體]相關值
    indirect case addition(ArithmeticExpression,
                    ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression,
                    ArithmeticExpression)
}

// 或是你也可以把 indirect 加在 enum 前面
// 表示整個列舉都是可以遞迴的
indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression,
                          ArithmeticExpression)
}

接者使用一個遞迴函式來示範這個遞迴列舉:

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case .number(let value):
        return value
    case .addition(let left, let right):
        return evaluate(left) + evaluate(right)
    case .multiplication(let left, let right):
        return evaluate(left) * evaluate(right)
    }
}

// 計算 (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))

// 印出:18
print(evaluate(product))

上述程式可以看到,當函式的參數為純數字,會直接返回該數字的值。而如果是加法或乘法運算,則是分別計算兩個表達式的值後,再相加或相乘。

範例

本節範例程式碼放在 ch2/enumerations.playground

results matching ""

    No results matching ""