Swift-列舉

優質神經病發表於2018-07-25

列舉為一組相關的值定義了一個共同的型別,使我們在程式碼中以型別安全的方式來使用這些值。

Swift中的列舉非常靈活,不必給每一個列舉成員提供一個值。如果給列舉成員提供一個原始值,則該值的型別可以是字串,字元,或者是一個整形值或者浮點值。

此外,列舉成員可以指定任意型別的關聯值儲存到列舉成員中,就像其他語言中的聯合體和變數。可以在一個列舉中定義一組相關的列舉成員,每一個列舉成員都可以有適當型別的關聯值。

在Swift中,列舉型別是一等型別。它們採用了很多在傳統上只被類所支援的特性,例如計算屬性,用於提供列舉值的附加資訊,例項方法,用於提供和列舉值相聯的功能。列舉也可以定義建構函式來提供一個初始值,可以在原始實現的基礎上擴充套件它們的功能,還可以遵循協議來提供標準功能。

本文涉及到的內容有:列舉語法使用Switch語句匹配列舉值關聯值原始值遞迴列舉

列舉語法

使用enum關鍵詞來建立列舉並且把它們的整個定義放在一對大括號內,如下:

enum CompassPoint {
	case north
	case south
	case east
	case west
}
複製程式碼

列舉中定義的值(如:northsoutheastwest)是這個列舉的成員。可以使用case關鍵字來定義一個新的列舉成員值。

注: Swift的列舉成員子在被建立時不會被賦予一個預設的整形值。在上面的CompassPoint例子中,northsoutheadtwest不會被隱式的賦值為0123。相反,這些列舉成員本身就是完備的值,這些值的型別是已經明確定義好的CompassPoint型別。

多個成員值可以出現在同一行上,用逗號隔開,如下:

enum Planet {
	case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
複製程式碼

每個列舉定義了一個全新的型別。像Swift中其他型別一樣。它們的名字應該以一個大寫字母開頭。給列舉型別起一個單數名字而不是複數名字,以便於:

var directionToHead = CompassPoint.west
directionToHead = .east
複製程式碼

當申明的變數的型別被已知時,再次為其賦值可以省略列舉型別名。

使用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")
}
// 列印 "Watch out for penguins”
複製程式碼

注: 上述列舉中並未使用default分支,因為列舉了所有情況,便可以省略default分支,不然會編譯錯誤

關聯值

可以定義Swift列舉來儲存任意型別的關聯值,如果需要的話,每個列舉成員的關聯值型別可以各不相同。列舉的這種特性跟其他語言中的可識別聯合,標籤聯合或者變體相似。

例如,假設一個庫存跟蹤系統需要利用兩種不同型別的條形碼來跟蹤商品。有些商品上標有使用09的數字的URC格式的一維條形碼。另外一種是QR碼格式的二維碼,它可以使用任何ISO 8859-1 字元,並且可以編碼一個最多擁有2953個字元的字串,在Swift中,使用如下方式定義表示兩種商品條形碼的列舉:

enum Barcode {
	case upc(Int, Int, Int, Int)
	case qrCode(String)
}
複製程式碼

以上程式碼可以這麼理解:“定義一個名為Barcode的列舉型別,它的一個成員值是具有(Int, Int, Int, Int)型別關聯值的upc,另一個成員值是具有String型別關聯值的qrCode。”

這個定義不提供任何IntString型別的關聯值,它只是定義了,當Barcode常量和變數等於Barcode.upcBarcode.quCode時,可以儲存的相關值的型別。

var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
複製程式碼

像先前那樣,可以使用一個switch語句來檢查不同的條形碼型別。然而,這一次,關聯值可以被提取出來作為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).")
}
// 列印 "QR code: ABCDEFGHIJKLMNOP."
複製程式碼

如果一個列舉成員的所有關聯值都被提取為常量,或者都被提取為常量,為了簡潔,你可以只在成員名稱前標註一個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).")
}
// 輸出 "QR code: ABCDEFGHIJKLMNOP."
複製程式碼

原始值

作為關聯值的替代選擇,列舉成員可以被原始值(預設值)預填充,這些原始值的型別必須相同。 如下:

enum ASCIIControlCharacter: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}
複製程式碼

列舉型別 ASCIIControlCharacter 的原始值型別被定義為 Character,並設定了一些比較常見的 ASCII 控制字元。

原始值可以是字串、字元,或者任意整形值或浮點值。每個原始值在列舉宣告中必須是唯一的。

注: 原始值和關聯值是不同的。原始值是在定義列舉時被預填充的值,像上述三個 ASCII 碼。對於一個特定的列舉成員,它的原始值始終不變。關聯值是建立一個基於列舉成員的常量或變數時才設定的值,列舉成員的關聯值可以變化。

原始值的隱藏賦值

在使用原始值為整數護著字串型別的列舉時,不需要顯式的為每一個列舉成員設定原始值,Swift會自動賦值。

例如,當使用整數作為原始值時,隱式賦值的依次遞增1,如果第一個列舉成員沒有設定原始值,其原始值為0

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
複製程式碼

在上面的例子中,Plant.mercury 的顯式原始值為 1Planet.venus 的隱式原始值為 2,依次類推。

而當使用字串作為列舉型別的原始值時,每個列舉成員的隱式原始值為該列舉成員的名稱。下面的例子是 CompassPoint 列舉的細化,使用字串型別的原始值來表示各個方向的名稱:

enum CompassPoint: String {
    case north, south, east, west
}
複製程式碼

上面例子中,CompassPoint.south 擁有隱式原始值 south,依次類推。

使用列舉成員的 rawValue 屬性可以訪問該列舉成員的原始值:

let earthsOrder = Planet.earth.rawValue
// earthsOrder 值為 3

let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection 值為 "west"
複製程式碼

使用原始值初始化列舉例項

如果在定義列舉型別的時候使用了原始值,那麼將會自動獲得一個初始化方法,這個方法接受一個叫做rawValue的引數,引數型別即為原始值型別,返回值則是列舉成員或者nil

這個例子利用原始值7建立了列舉成員uranus如下:

let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet 型別為 Planet? 值為 Planet.uranus
複製程式碼

然而,並非所有Int值都能可以找到一個匹配的行星。因此原始值構造器總是返回一個可選的列舉成員。在上面的例子中,possiblePlanetPlanet?型別,或者說可選的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)")
}
// 輸出 "There isn't a planet at position 11
複製程式碼

遞迴列舉

遞迴列舉是一種列舉型別,它有一個或多個列舉成員使用該列舉型別的例項作為關聯值。使用遞迴列舉時,編譯器會插入一個間接層。你可以在列舉成員前加上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)
}
複製程式碼

上面定義的列舉型別可以儲存三種算術表示式:純數字、兩個表示式相加、兩個表示式想乘。列舉成員additionmultiplication的關聯值也是算術表示式--這些關聯值使得巢狀表示式成為可能。例如,表示式(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))
// 列印 "18"
複製程式碼

該函式如果遇到純數字,就直接返回該數字的值。如果遇到的是加法或乘法運算,則分別計算左邊表示式和右邊表示式的值,然後相加或相乘