title: "Swift 中枚舉高級(jí)用法及實(shí)踐"
date: 2015-11-20
tags: [APPVENTURE]
categories: [Swift 進(jìn)階]
permalink: advanced-practical-enum-examples
原文鏈接=http://appventure.me/2015/10/17/advanced-practical-enum-examples/
作者=Benedikt Terhechte
原文日期=2015-10-17
譯者=pmst+小鍋
校對(duì)=shanks
定稿=shanks
譯者注:作為一個(gè)走心且有逼格的翻譯組,我們對(duì)本篇文章中的代碼都進(jìn)行了驗(yàn)證,并且寫了將代碼分為上下兩篇做成了 playground,代碼中有詳盡的注釋。可以到這個(gè)github地址上進(jìn)行下載,這個(gè)代碼由翻譯組的另一位小伙伴 ppt 提供。
本文是一篇詳細(xì)且具有實(shí)戰(zhàn)意義的教程,涵蓋幾乎所有枚舉(Enum)知識(shí)點(diǎn),為你解答Swift中枚舉的應(yīng)用場(chǎng)合以及使用方法。
和switch語(yǔ)句類似,Swift中的枚舉乍看之下更像是C語(yǔ)言中枚舉的進(jìn)階版本,即允許你定義一種類型,用于表示普通事情中某種用例。不過(guò)深入挖掘之后,憑借Swift背后特別的設(shè)計(jì)理念,相比較C語(yǔ)言枚舉來(lái)說(shuō)其在實(shí)際場(chǎng)景中的應(yīng)用更為廣泛。特別是作為強(qiáng)大的工具,Swift中的枚舉能夠清晰表達(dá)代碼的意圖。
本文中,我們將首先了解基礎(chǔ)語(yǔ)法和使用枚舉的可能性,接著通過(guò)實(shí)戰(zhàn)教你如何以及何時(shí)使用枚舉。最后我們還會(huì)大致了解下Swift標(biāo)準(zhǔn)庫(kù)中枚舉是如何被使用的。
正式開始學(xué)習(xí)之前,先給出枚舉的定義。之后我們將回過(guò)頭再來(lái)討論它。
枚舉聲明的類型是囊括可能狀態(tài)的有限集,且可以具有附加值。通過(guò)內(nèi)嵌(nesting),方法(method),關(guān)聯(lián)值(associated values)和模式匹配(pattern matching),枚舉可以分層次地定義任何有組織的數(shù)據(jù)。
深入理解(Diving In)
簡(jiǎn)要概述如何定義和使用枚舉。
定義基本的枚舉類型(Defining Basic Enums)
試想我們正在開發(fā)一款游戲,玩家能夠朝四個(gè)方向移動(dòng)。所以嘍,玩家的運(yùn)動(dòng)軌跡受到了限制。顯然,我們能夠使用枚舉來(lái)表述這一情況:
case Left
case Right
case Top
case Bottom
}```
緊接著,你可以使用多種模式匹配結(jié)構(gòu)獲取到Movement的枚舉值,或者按照特定情況執(zhí)行操作:
let aMovement = Movement.Left
// switch 分情況處理
switch aMovement{
case .Left: print("left")
default:()
}
// 明確的case情況
if case .Left = aMovement{
print("left")
}
if aMovement == .Left { print("left") }```
案例中,我們無(wú)須明確指出enum的實(shí)際名稱(即case Move.Left:print("Left"))。因?yàn)轭愋蜋z查器能夠自動(dòng)為此進(jìn)行類型推算。這對(duì)于那些UIKit以及AppKit中錯(cuò)綜復(fù)雜的枚舉是灰常有用的。
枚舉值(Enum Values)
當(dāng)然,你可能想要為enum中每個(gè)case分配一個(gè)值。這相當(dāng)有用,比如枚舉自身實(shí)際與某事或某物掛鉤時(shí),往往這些東西又需要使用不同類型來(lái)表述。在C語(yǔ)言中,你只能為枚舉case分配整型值,而Swift則提供了更多的靈活性。
// 映射到整型
enum Movement: Int {
case Left = 0
case Right = 1
case Top = 2
case Bottom = 3
}
// 同樣你可以與字符串一一對(duì)應(yīng)
enum House: String {
case Baratheon = "Ours is the Fury"
case Greyjoy = "We Do Not Sow"
case Martell = "Unbowed, Unbent, Unbroken"
case Stark = "Winter is Coming"
case Tully = "Family, Duty, Honor"
case Tyrell = "Growing Strong"
}
// 或者float double都可以(同時(shí)注意枚舉中的花式unicode)
enum Constants: Double {
case π = 3.14159
case e = 2.71828
case φ = 1.61803398874
case λ = 1.30357
}```
對(duì)于String和Int類型來(lái)說(shuō),你甚至可以忽略為枚舉中的case賦值,Swift編譯器也能正常工作。
// Mercury = 1, Venus = 2, ... Neptune = 8
enum Planet: Int {
case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}
// North = "North", ... West = "West"
// 譯者注: 這個(gè)是swift2.0新增語(yǔ)法
enum CompassPoint: String {
case North, South, East, West
}```
Swift枚舉中支持以下四種關(guān)聯(lián)值類型:
整型(Integer)
浮點(diǎn)數(shù)(Float Point)
字符串(String)
布爾類型(Boolean)
因此你無(wú)法1為枚舉分配諸如CGPoint類型的值。
倘若你想要讀取枚舉的值,可以通過(guò)rawValue屬性來(lái)實(shí)現(xiàn):
let bestHouse = House.Stark
print(bestHouse.rawValue)
// prints "Winter is coming"
不過(guò)某種情形下,你可能想要通過(guò)一個(gè)已有的raw value來(lái)創(chuàng)建一個(gè)enum case。這種情況下,枚舉提供了一個(gè)指定構(gòu)造方法:
enum Movement: Int {
case Left = 0
case Right = 1
case Top = 2
case Bottom = 3
}
// 創(chuàng)建一個(gè)movement.Right 用例,其raw value值為1
let rightMovement = Movement(rawValue: 1)
倘若使用rawValue構(gòu)造器,切記它是一個(gè)可失敗構(gòu)造器(failable initializer)。換言之,構(gòu)造方法返回值為可選類型值,因?yàn)橛袝r(shí)候傳入的值可能與任意一個(gè)case都不匹配。比如Movement(rawValue:42)。
如果你想要以底層 C 二進(jìn)制編碼形式呈現(xiàn)某物或某事,使得更具可讀性,這是一個(gè)非常有用的功能。例如,可以看一下BSD kqeue library中的VNode Flags標(biāo)志位的編碼方式:
enum VNodeFlags : UInt32 {
case Delete = 0x00000001
case Write = 0x00000002
case Extended = 0x00000004
case Attrib = 0x00000008
case Link = 0x00000010
case Rename = 0x00000020
case Revoke = 0x00000040
case None = 0x00000080
}
如此便可以使你的Delete或Write用例聲明一目了然,稍后一旦需要,只需將raw value傳入 C 函數(shù)中即可。
嵌套枚舉(Nesting Enums)
如果你有特定子類型的需求,可以對(duì)enum進(jìn)行嵌套。這樣就允許你為實(shí)際的enum中包含其他明確信息的enum。以RPG游戲中的每個(gè)角色為例,每個(gè)角色能夠擁有武器,因此所有角色都可以獲取同一個(gè)武器集合。而游戲中的其他實(shí)例則無(wú)法獲取這些武器(比如食人魔,它們僅使用棍棒)。
enum Character {
enum Weapon {
case Bow
case Sword
case Lance
case Dagger
}
enum Helmet {
case Wooden
case Iron
case Diamond
}
case Thief
case Warrior
case Knight
}
現(xiàn)在,你可以通過(guò)層級(jí)結(jié)構(gòu)來(lái)描述角色允許訪問的項(xiàng)目條。
let character = Character.Thief
let weapon = Character.Weapon.Bow
let helmet = Character.Helmet.Iron
包含枚舉(Containing Enums)
同樣地,你也能夠在structs或classes中內(nèi)嵌枚舉。接著上面的例子:
struct Character {
enum CharacterType {
case Thief
case Warrior
case Knight
}
enum Weapon {
case Bow
case Sword
case Lance
case Dagger
}
let type: CharacterType
let weapon: Weapon
}
let warrior = Character(type: .Warrior, weapon: .Sword)
同樣地,這也將有助于我們將相關(guān)的信息集中在一個(gè)位置。
關(guān)聯(lián)值(Associated Value)
關(guān)聯(lián)值是將額外信息附加到enum case中的一種極好的方式。打個(gè)比方,你正在開發(fā)一款交易引擎,可能存在買和賣兩種不同的交易類型。除此之外每手交易還要制定明確的股票名稱和交易數(shù)量:
簡(jiǎn)單例程(Simple Example)
enum Trade {
case Buy
case Sell
}
func trade(tradeType: Trade, stock: String, amount: Int) {}
然而股票的價(jià)值和數(shù)量顯然從屬于交易,讓他們作為獨(dú)立的參數(shù)顯得模棱兩可。你可能已經(jīng)想到要往struct中內(nèi)嵌一個(gè)枚舉了,不過(guò)關(guān)聯(lián)值提供了一種更清爽的解決方案:
enum Trade {
case Buy(stock: String, amount: Int)
case Sell(stock: String, amount: Int)
}
func trade(type: Trade) {}
模式匹配(Pattern Mathching)
如果你想要訪問這些值,模式匹配再次救場(chǎng):
let trade = Trade.Buy(stock: "APPL", amount: 500)
if case let Trade.Buy(stock, amount) = trade {
print("buy (amount) of (stock)")
}
標(biāo)簽(Labels)
關(guān)聯(lián)值不需要附加標(biāo)簽的聲明:
enum Trade {
case Buy(String, Int)
case Sell(String, Int)
}
倘若你添加了,那么,每當(dāng)創(chuàng)建枚舉用例時(shí),你都需要將這些標(biāo)簽標(biāo)示出來(lái)。
(元組參數(shù))Tuple as Arguments
更重要的是,Swift內(nèi)部相關(guān)信息其實(shí)是一個(gè)元組,所以你可以像下面這樣做:
let tp = (stock: "TSLA", amount: 100)
let trade = Trade.Sell(tp)
if case let Trade.Sell(stock, amount) = trade {
print("buy (amount) of (stock)")
}
// Prints: "buy 100 of TSLA"
語(yǔ)法允許您將元組當(dāng)作一個(gè)簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu),稍后元組將自動(dòng)轉(zhuǎn)換到高級(jí)類型,就比如enum case。想象一個(gè)應(yīng)用程序可以讓用戶來(lái)配置電腦:
typealias Config = (RAM: Int, CPU: String, GPU: String)
// Each of these takes a config and returns an updated config
func selectRAM(_ config: Config) -> Config {return (RAM: 32, CPU: config.CPU, GPU: config.GPU)}
func selectCPU(_ config: Config) -> Config {return (RAM: config.RAM, CPU: "3.2GHZ", GPU: config.GPU)}
func selectGPU(_ config: Config) -> Config {return (RAM: config.RAM, CPU: "3.2GHZ", GPU: "NVidia")}
enum Desktop {
case Cube(Config)
case Tower(Config)
case Rack(Config)
}
let aTower = Desktop.Tower(selectGPU(selectCPU(selectRAM((0, "", "") as Config))))
配置的每個(gè)步驟均通過(guò)遞交元組到enum中進(jìn)行內(nèi)容更新。倘若我們從函數(shù)式編程2中獲得啟發(fā),這將變得更好。
infix operator <^> { associativity left }
func <^>(a: Config, f: (Config) -> Config) -> Config {
return f(a)
}
最后,我們可以將不同配置步驟串聯(lián)起來(lái)。這在配置步驟繁多的情況下相當(dāng)有用。
let config = (0, "", "") <^> selectRAM <^> selectCPU <^> selectGPU
let aCube = Desktop.Cube(config)
使用案例(Use Case Example)
關(guān)聯(lián)值可以以多種方式使用。常言道:一碼勝千言, 下面就上幾段簡(jiǎn)單的示例代碼,這幾段代碼沒有特定的順序。
// 擁有不同值的用例
enum UserAction {
case OpenURL(url: NSURL)
case SwitchProcess(processId: UInt32)
case Restart(time: NSDate?, intoCommandLine: Bool)
}
// 假設(shè)你在實(shí)現(xiàn)一個(gè)功能強(qiáng)大的編輯器,這個(gè)編輯器允許多重選擇,
// 正如 Sublime Text : https://www.youtube.com/watch?v=i2SVJa2EGIw
enum Selection {
case None
case Single(Range<Int>)
case Multiple([Range<Int>])
}
// 或者映射不同的標(biāo)識(shí)碼
enum Barcode {
case UPCA(numberSystem: Int, manufacturer: Int, product: Int, check: Int)
case QRCode(productCode: String)
}
// 又或者假設(shè)你在封裝一個(gè) C 語(yǔ)言庫(kù),正如 Kqeue BSD/Darwin 通知系統(tǒng):
// https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
enum KqueueEvent {
case UserEvent(identifier: UInt, fflags: [UInt32], data: Int)
case ReadFD(fd: UInt, data: Int)
case WriteFD(fd: UInt, data: Int)
case VnodeFD(fd: UInt, fflags: [UInt32], data: Int)
case ErrorEvent(code: UInt, message: String)
}
// 最后, 一個(gè) RPG 游戲中的所有可穿戴裝備可以使用一個(gè)枚舉來(lái)進(jìn)行映射,
// 可以為一個(gè)裝備增加重量和持久兩個(gè)屬性
// 現(xiàn)在可以僅用一行代碼來(lái)增加一個(gè)"鉆石"屬性,如此一來(lái)我們便可以增加幾件新的鑲嵌鉆石的可穿戴裝備
enum Wearable {
enum Weight: Int {
case Light = 1
case Mid = 4
case Heavy = 10
}
enum Armor: Int {
case Light = 2
case Strong = 8
case Heavy = 20
}
case Helmet(weight: Weight, armor: Armor)
case Breastplate(weight: Weight, armor: Armor)
case Shield(weight: Weight, armor: Armor)
}
let woodenHelmet = Wearable.Helmet(weight: .Light, armor: .Light)
方法和屬性(Methods and properties)
你也可以在enum中像這樣定義方法:
enum Wearable {
enum Weight: Int {
case Light = 1
}
enum Armor: Int {
case Light = 2
}
case Helmet(weight: Weight, armor: Armor)
func attributes() -> (weight: Int, armor: Int) {
switch self {
case .Helmet(let w, let a): return (weight: w.rawValue * 2, armor: w.rawValue * 4)
}
}
}
let woodenHelmetProps = Wearable.Helmet(weight: .Light, armor: .Light).attributes()
print (woodenHelmetProps)
// prints "(2, 4)"
枚舉中的方法為每一個(gè)enum case而“生”。所以倘若想要在特定情況執(zhí)行特定代碼的話,你需要分支處理或采用switch語(yǔ)句來(lái)明確正確的代碼路徑。
enum Device {
case iPad, iPhone, AppleTV, AppleWatch
func introduced() -> String {
switch self {
case AppleTV: return "(self) was introduced 2006"
case iPhone: return "(self) was introduced 2007"
case iPad: return "(self) was introduced 2010"
case AppleWatch: return "(self) was introduced 2014"
}
}
}
print (Device.iPhone.introduced())
// prints: "iPhone was introduced 2007"
屬性(Properties)
盡管增加一個(gè)存儲(chǔ)屬性到枚舉中不被允許,但你依然能夠創(chuàng)建計(jì)算屬性。當(dāng)然,計(jì)算屬性的內(nèi)容都是建立在枚舉值下或者枚舉關(guān)聯(lián)值得到的。
enum Device {
case iPad, iPhone
var year: Int {
switch self {
case iPhone: return 2007
case iPad: return 2010
}
}
}
靜態(tài)方法(Static Methods)
你也能夠?yàn)槊杜e創(chuàng)建一些靜態(tài)方法(static methods)。換言之通過(guò)一個(gè)非枚舉類型來(lái)創(chuàng)建一個(gè)枚舉。在這個(gè)示例中,我們需要考慮用戶有時(shí)將蘋果設(shè)備叫錯(cuò)的情況(比如AppleWatch叫成iWatch),需要返回一個(gè)合適的名稱。
enum Device {
case AppleWatch
static func fromSlang(term: String) -> Device? {
if term == "iWatch" {
return .AppleWatch
}
return nil
}
}
print (Device.fromSlang("iWatch"))
可變方法(Mutating Methods)
方法可以聲明為mutating。這樣就允許改變隱藏參數(shù)self的case值了3。
enum TriStateSwitch {
case Off, Low, High
mutating func next() {
switch self {
case Off:
self = Low
case Low:
self = High
case High:
self = Off
}
}
}
var ovenLight = TriStateSwitch.Low
ovenLight.next()
// ovenLight 現(xiàn)在等于.On
ovenLight.next()
// ovenLight 現(xiàn)在等于.Off
小結(jié)(To Recap)
至此,我們已經(jīng)大致了解了Swift中枚舉語(yǔ)法的基本用例。在開始邁向進(jìn)階之路之前,讓我們重新審視文章開篇給出的定義,看看現(xiàn)在是否變得更清晰了。
枚舉聲明的類型是囊括可能狀態(tài)的有限集,且可以具有附加值。通過(guò)內(nèi)嵌(nesting),方法(method),關(guān)聯(lián)值(associated values)和模式匹配(pattern matching),枚舉可以分層次地定義任何有組織的數(shù)據(jù)。
現(xiàn)在我們已經(jīng)對(duì)這個(gè)定義更加清晰了。確實(shí),如果我們添加關(guān)聯(lián)值和嵌套,enum就看起來(lái)就像一個(gè)封閉的、簡(jiǎn)化的struct。相比較struct,前者優(yōu)勢(shì)體現(xiàn)在能夠?yàn)榉诸惻c層次結(jié)構(gòu)編碼。
// Struct Example
struct Point { let x: Int, let y: Int }
struct Rect { let x: Int, let y: Int, let width: Int, let height: Int }
// Enum Example
enum GeometricEntity {
case Point(x: Int, y: Int)
case Rect(x: Int, y: Int, width: Int, height: Int)
}
方法和靜態(tài)方法的添加允許我們?yōu)閑num附加功能,這意味著無(wú)須依靠額外函數(shù)就能實(shí)現(xiàn)4。
// C-Like example
enum Trade {
case Buy
case Sell
}
func order(trade: Trade)
// Swift Enum example
enum Trade {
case Buy
case Sell
func order()
}
枚舉進(jìn)階(Advanced Enum Usage)
協(xié)議(Protocols)
我已經(jīng)提及了structs和enums之間的相似性。除了附加方法的能力之外,Swift也允許你在枚舉中使用協(xié)議(Protocols)和協(xié)議擴(kuò)展(Protocol Extension)。
Swift協(xié)議定義一個(gè)接口或類型以供其他數(shù)據(jù)結(jié)構(gòu)來(lái)遵循。enum當(dāng)然也不例外。我們先從Swift標(biāo)準(zhǔn)庫(kù)中的一個(gè)例子開始.
CustomStringConvertible是一個(gè)以打印為目的的自定義格式化輸出的類型。
protocol CustomStringConvertible {
var description: String { get }
}
該協(xié)議只有一個(gè)要求,即一個(gè)只讀(getter)類型的字符串(String類型)。我們可以很容易為enum實(shí)現(xiàn)這個(gè)協(xié)議。
enum Trade: CustomStringConvertible {
case Buy, Sell
var description: String {
switch self {
case Buy: return "We're buying something"
case Sell: return "We're selling something"
}
}
}
let action = Trade.Buy
print("this action is (action)")
// prints: this action is We're buying something
一些協(xié)議的實(shí)現(xiàn)可能需要根據(jù)內(nèi)部狀態(tài)來(lái)相應(yīng)處理要求。例如定義一個(gè)管理銀行賬號(hào)的協(xié)議。
protocol AccountCompatible {
var remainingFunds: Int { get }
mutating func addFunds(amount: Int) throws
mutating func removeFunds(amount: Int) throws
}
你也許會(huì)簡(jiǎn)單地拿struct實(shí)現(xiàn)這個(gè)協(xié)議,但是考慮應(yīng)用的上下文,enum是一個(gè)更明智的處理方法。不過(guò)你無(wú)法添加一個(gè)存儲(chǔ)屬性到enum中,就像var remainingFuns:Int。那么你會(huì)如何構(gòu)造呢?答案灰常簡(jiǎn)單,你可以使用關(guān)聯(lián)值完美解決:
enum Account {
case Empty
case Funds(remaining: Int)
enum Error: ErrorType {
case Overdraft(amount: Int)
}
var remainingFunds: Int {
switch self {
case Empty: return 0
case Funds(let remaining): return remaining
}
}
}
為了保持代碼清爽,我們可以在enum的協(xié)議擴(kuò)展(protocl extension)中定義必須的協(xié)議函數(shù):
extension Account: AccountCompatible {
mutating func addFunds(amount: Int) throws {
var newAmount = amount
if case let .Funds(remaining) = self {
newAmount += remaining
}
if newAmount < 0 {
throw Error.Overdraft(amount: -newAmount)
} else if newAmount == 0 {
self = .Empty
} else {
self = .Funds(remaining: newAmount)
}
}
mutating func removeFunds(amount: Int) throws {
try self.addFunds(amount * -1)
}
}
var account = Account.Funds(remaining: 20)
print("add: ", try? account.addFunds(10))
print ("remove 1: ", try? account.removeFunds(15))
print ("remove 2: ", try? account.removeFunds(55))
// prints:
// : add: Optional(())
// : remove 1: Optional(())
// : remove 2: nil
正如你所看見的,我們通過(guò)將值存儲(chǔ)到enum cases中實(shí)現(xiàn)了協(xié)議所有要求項(xiàng)。如此做法還有一個(gè)妙不可言的地方:現(xiàn)在整個(gè)代碼基礎(chǔ)上你只需要一個(gè)模式匹配就能測(cè)試空賬號(hào)輸入的情況。你不需要關(guān)心剩余資金是否等于零。
同時(shí),我們也在賬號(hào)(Accout)中內(nèi)嵌了一個(gè)遵循ErrorType協(xié)議的枚舉,這樣我們就可以使用Swift2.0語(yǔ)法來(lái)進(jìn)行錯(cuò)誤處理了。這里給出更詳細(xì)的使用案例教程。
擴(kuò)展(Extensions)
正如剛才所見,枚舉也可以進(jìn)行擴(kuò)展。最明顯的用例就是將枚舉的case和method分離,這樣閱讀你的代碼能夠簡(jiǎn)單快速地消化掉enum內(nèi)容,緊接著轉(zhuǎn)移到方法定義:
enum Entities {
case Soldier(x: Int, y: Int)
case Tank(x: Int, y: Int)
case Player(x: Int, y: Int)
}
現(xiàn)在,我們?yōu)閑num擴(kuò)展方法:
extension Entities {
mutating func move(dist: CGVector) {}
mutating func attack() {}
}
你同樣可以通過(guò)寫一個(gè)擴(kuò)展來(lái)遵循一個(gè)特定的協(xié)議:
extension Entities: CustomStringConvertible {
var description: String {
switch self {
case let .Soldier(x, y): return "(x), (y)"
case let .Tank(x, y): return "(x), (y)"
case let .Player(x, y): return "(x), (y)"
}
}
}
枚舉泛型(Generic Enums)
枚舉也支持泛型參數(shù)定義。你可以使用它們以適應(yīng)枚舉中的關(guān)聯(lián)值。就拿直接來(lái)自Swift標(biāo)準(zhǔn)庫(kù)中的簡(jiǎn)單例子來(lái)說(shuō),即Optional類型。你主要可能通過(guò)以下幾種方式使用它:可選鏈(optional chaining(?))、if-let可選綁定、guard let、或switch,但是從語(yǔ)法角度來(lái)說(shuō)你也可以這么使用Optional:
let aValue = Optional<Int>.Some(5)
let noValue = Optional<Int>.None
if noValue == Optional.None { print("No value") }
這是Optional最直接的用例,并未使用任何語(yǔ)法糖,但是不可否認(rèn)Swift中語(yǔ)法糖的加入使得你的工作更簡(jiǎn)單。如果你觀察上面的實(shí)例代碼,你恐怕已經(jīng)猜到Optional內(nèi)部實(shí)現(xiàn)是這樣的5:
// Simplified implementation of Swift's Optional
enum MyOptional<T> {
case Some(T)
case None
}
這里有啥特別呢?注意枚舉的關(guān)聯(lián)值采用泛型參數(shù)T作為自身類型,這樣可選類型構(gòu)造任何你想要的返回值。
枚舉可以擁有多個(gè)泛型參數(shù)。就拿熟知的Either類為例,它并非是Swift標(biāo)準(zhǔn)庫(kù)中的一部分,而是實(shí)現(xiàn)于眾多開源庫(kù)以及
其他函數(shù)式編程語(yǔ)言,比如Haskell或F#。設(shè)計(jì)想法是這樣的:相比較僅僅返回一個(gè)值或沒有值(née Optional),你更期望返回一個(gè)成功值或者一些反饋信息(比如錯(cuò)誤值)。
// The well-known either type is, of course, an enum that allows you to return either
// value one (say, a successful value) or value two (say an error) from a function
enum Either<T1, T2> {
case Left(T1)
case Right(T2)
}
最后,Swift中所有在class和struct中奏效的類型約束,在enum中同樣適用。
// Totally nonsensical example. A bag that is either full (has an array with contents)
// or empty.
enum Bag<T: SequenceType where T.Generator.Element==Equatable> {
case Empty
case Full(contents: T)
}
遞歸 / 間接(Indirect)類型
間接類型是 Swift 2.0 新增的一個(gè)類型。 它們?cè)试S將枚舉中一個(gè) case 的關(guān)聯(lián)值再次定義為枚舉。舉個(gè)例子,假設(shè)我們想定義一個(gè)文件系統(tǒng),用來(lái)表示文件以及包含文件的目錄。如果將文件和目錄定義為枚舉的 case,則目錄 case 的關(guān)聯(lián)值應(yīng)該再包含一個(gè)文件的數(shù)組作為它的關(guān)聯(lián)值。因?yàn)檫@是一個(gè)遞歸的操作,編譯器需要對(duì)此進(jìn)行一個(gè)特殊的準(zhǔn)備。Swift 文檔中是這么寫的:
枚舉和 case 可以被標(biāo)記為間接的(indrect),這意味它們的關(guān)聯(lián)值是被間接保存的,這允許我們定義遞歸的數(shù)據(jù)結(jié)構(gòu)。
所以,如果我們要定義 FileNode 的枚舉,它應(yīng)該會(huì)是這樣的:
enum FileNode {
case File(name: String)
indirect case Folder(name: String, files: [FileNode])
}
此處的 indrect 關(guān)鍵字告訴編譯器間接地處理這個(gè)枚舉的 case。也可以對(duì)整個(gè)枚舉類型使用這個(gè)關(guān)鍵字。作為例子,我們來(lái)定義一個(gè)二叉樹:
indirect enum Tree<Element: Comparable> {
case Empty
case Node(Tree<Element>,Element,Tree<Element>)
}
這是一個(gè)很強(qiáng)大的特性,可以讓我們用非常簡(jiǎn)潔的方式來(lái)定義一個(gè)有著復(fù)雜關(guān)聯(lián)的數(shù)據(jù)結(jié)構(gòu)。
使用自定義類型作為枚舉的值
如果我們忽略關(guān)聯(lián)值,則枚舉的值就只能是整型,浮點(diǎn)型,字符串和布爾類型。如果想要支持別的類型,則可以通過(guò)實(shí)現(xiàn) StringLiteralConvertible 協(xié)議來(lái)完成,這可以讓我們通過(guò)對(duì)字符串的序列化和反序列化來(lái)使枚舉支持自定義類型。
作為一個(gè)例子,假設(shè)我們要定義一個(gè)枚舉來(lái)保存不同的 iOS 設(shè)備的屏幕尺寸:
enum Devices: CGSize {
case iPhone3GS = CGSize(width: 320, height: 480)
case iPhone5 = CGSize(width: 320, height: 568)
case iPhone6 = CGSize(width: 375, height: 667)
case iPhone6Plus = CGSize(width: 414, height: 736)
}
然而,這段代碼不能通過(guò)編譯。因?yàn)?CGPoint 并不是一個(gè)常量,不能用來(lái)定義枚舉的值。我們需要為想要支持的自定義類型增加一個(gè)擴(kuò)展,讓其實(shí)現(xiàn) StringLiteralConvertible 協(xié)議。這個(gè)協(xié)議要求我們實(shí)現(xiàn)三個(gè)構(gòu)造方法,這三個(gè)方法都需要使用一個(gè)String類型的參數(shù),并且我們需要將這個(gè)字符串轉(zhuǎn)換成我們需要的類型(此處是CGSize)。
extension CGSize: StringLiteralConvertible {
public init(stringLiteral value: String) {
let size = CGSizeFromString(value)
self.init(width: size.width, height: size.height)
}
public init(extendedGraphemeClusterLiteral value: String) {
let size = CGSizeFromString(value)
self.init(width: size.width, height: size.height)
}
public init(unicodeScalarLiteral value: String) {
let size = CGSizeFromString(value)
self.init(width: size.width, height: size.height)
}
}
現(xiàn)在就可以來(lái)實(shí)現(xiàn)我們需要的枚舉了,不過(guò)這里有一個(gè)缺點(diǎn):初始化的值必須寫成字符串形式,因?yàn)檫@就是我們定義的枚舉需要接受的類型(記住,我們實(shí)現(xiàn)了 StringLiteralConvertible,因此String可以轉(zhuǎn)化成CGSize類型)
enum Devices: CGSize {
case iPhone3GS = "{320, 480}"
case iPhone5 = "{320, 568}"
case iPhone6 = "{375, 667}"
case iPhone6Plus = "{414, 736}"
}
終于,我們可以使用 CGPoint 類型的枚舉了。需要注意的是,當(dāng)要獲取真實(shí)的 CGPoint 的值的時(shí)候,我們需要訪問枚舉的是 rawValue 屬性。
let a = Devices.iPhone5
let b = a.rawValue
print("the phone size string is (a), width is (b.width), height is (b.height)")
// prints : the phone size string is iPhone5, width is 320.0, height is 568.0
使用字符串序列化的形式,會(huì)讓使用自定義類型的枚舉比較困難,然而在某些特定的情況下,這也會(huì)給我們?cè)黾硬簧俦憷?比較使用NSColor / UIColor的時(shí)候)。不僅如此,我們完全可以對(duì)自己定義的類型使用這個(gè)方法。
對(duì)枚舉的關(guān)聯(lián)值進(jìn)行比較
在通常情況下,枚舉是很容易進(jìn)行相等性判斷的。一個(gè)簡(jiǎn)單的 enum T { case a, b } 實(shí)現(xiàn)默認(rèn)支持相等性判斷 T.a == T.b, T.b != T.a
然而,一旦我們?yōu)槊杜e增加了關(guān)聯(lián)值,Swift 就沒有辦法正確地為兩個(gè)枚舉進(jìn)行相等性判斷,需要我們自己實(shí)現(xiàn) == 運(yùn)行符。這并不是很困難:
enum Trade {
case Buy(stock: String, amount: Int)
case Sell(stock: String, amount: Int)
}
func ==(lhs: Trade, rhs: Trade) -> Bool {
switch (lhs, rhs) {
case let (.Buy(stock1, amount1), .Buy(stock2, amount2))
where stock1 == stock2 && amount1 == amount2:
return true
case let (.Sell(stock1, amount1), .Sell(stock2, amount2))
where stock1 == stock2 && amount1 == amount2:
return true
default: return false
}
}
正如我們所見,我們通過(guò) switch 語(yǔ)句對(duì)兩個(gè)枚舉的 case 進(jìn)行判斷,并且只有當(dāng)它們的 case 是匹配的時(shí)候(比如 Buy 和 Buy)才對(duì)它們的真實(shí)關(guān)聯(lián)值進(jìn)行判斷。
自定義構(gòu)造方法
在 靜態(tài)方法 一節(jié)當(dāng)中我們已經(jīng)提到它們可以作為從不同數(shù)據(jù)構(gòu)造枚舉的方便形式。在之前的例子里也展示過(guò),對(duì)出版社經(jīng)常誤用的蘋果設(shè)備名返回正確的名字:
enum Device {
case AppleWatch
static func fromSlang(term: String) -> Device? {
if term == "iWatch" {
return .AppleWatch
}
return nil
}
}
我們也可以使用自定義構(gòu)造方法來(lái)替換靜態(tài)方法。枚舉與結(jié)構(gòu)體和類的構(gòu)造方法最大的不同在于,枚舉的構(gòu)造方法需要將隱式的 self 屬性設(shè)置為正確的 case。
enum Device {
case AppleWatch
init?(term: String) {
if term == "iWatch" {
self = .AppleWatch
}
return nil
}
}
在這個(gè)例子中,我們使用了可失敗(failable)的構(gòu)造方法。但是,普通的構(gòu)造方法也可以工作得很好:
enum NumberCategory {
case Small
case Medium
case Big
case Huge
init(number n: Int) {
if n < 10000 { self = .Small }
else if n < 1000000 { self = .Medium }
else if n < 100000000 { self = .Big }
else { self = .Huge }
}
}
let aNumber = NumberCategory(number: 100)
print(aNumber)
// prints: "Small"
對(duì)枚舉的 case 進(jìn)行迭代
一個(gè)特別經(jīng)常被問到的問題就是如何對(duì)枚舉中的 case 進(jìn)行迭代。可惜的是,枚舉并沒有遵守SequenceType協(xié)議,因此沒有一個(gè)官方的做法來(lái)對(duì)其進(jìn)行迭代。取決于枚舉的類型,對(duì)其進(jìn)行迭代可能也簡(jiǎn)單,也有可能很困難。在StackOverflow上有一個(gè)很好的討論貼。貼子里面討論到的不同情況太多了,如果只在這里摘取一些會(huì)有片面性,而如果將全部情況都列出來(lái),則會(huì)太多。
對(duì) Objective-C 的支持
基于整型的枚舉,如 enum Bit: Int { case Zero = 0; case One = 1 } 可以通過(guò) @objc 標(biāo)識(shí)來(lái)將其橋接到 Objective-C 當(dāng)中。然而,一旦使用整型之外的類型(如 String)或者開始使用關(guān)聯(lián)值,我們就無(wú)法在 Objective-C 當(dāng)中使用這些枚舉了。
有一個(gè)名為_ObjectiveCBridgeable的隱藏協(xié)議,可以讓規(guī)范我們以定義合適的方法,如此一來(lái),Swift 便可以正確地將枚舉轉(zhuǎn)成 Objective-C 類型,但我猜這個(gè)協(xié)議被隱藏起來(lái)一定是有原因的。然而,從理論上來(lái)講,這個(gè)協(xié)議還是允許我們將枚舉(包括其實(shí)枚舉值)正確地橋接到 Objective-C 當(dāng)中。
但是,我們并不一定非要使用上面提到的這個(gè)方法。為枚舉添加兩個(gè)方法,使用 @objc 定義一個(gè)替代類型,如此一來(lái)我們便可以自由地將枚舉進(jìn)行轉(zhuǎn)換了,并且這種方式不需要遵守私有協(xié)議:
enum Trade {
case Buy(stock: String, amount: Int)
case Sell(stock: String, amount: Int)
}
// 這個(gè)類型也可以定義在 Objective-C 的代碼中
@objc class OTrade: NSObject {
var type: Int
var stock: String
var amount: Int
init(type: Int, stock: String, amount: Int) {
self.type = type
self.stock = stock
self.amount = amount
}
}
extension Trade {
func toObjc() -> OTrade {
switch self {
case let .Buy(stock, amount):
return OTrade(type: 0, stock: stock, amount: amount)
case let .Sell(stock, amount):
return OTrade(type: 1, stock: stock, amount: amount)
}
}
static func fromObjc(source: OTrade) -> Trade? {
switch (source.type) {
case 0: return Trade.Buy(stock: source.stock, amount: source.amount)
case 1: return Trade.Sell(stock: source.stock, amount: source.amount)
default: return nil
}
}
}
這個(gè)方法有一個(gè)的缺點(diǎn),我們需要將枚舉映射為 Objective-C 中的 NSObject 基礎(chǔ)類型(我們也可以直接使用 NSDictionary),但是,當(dāng)我們碰到一些確實(shí)需要在 Objective-C 當(dāng)中獲取有關(guān)聯(lián)值的枚舉時(shí),這是一個(gè)可以使用的方法。
枚舉底層
Erica Sadun 寫過(guò)一篇很流弊的關(guān)于枚舉底層的博客,涉及到枚舉底層的方方面面。在生產(chǎn)代碼中絕不應(yīng)該使用到這些東西,但是學(xué)習(xí)一下還是相當(dāng)有趣的。在這里,我準(zhǔn)備只提到那篇博客中一條,如果想了解更多,請(qǐng)移步到原文:
枚舉通常都是一個(gè)字節(jié)長(zhǎng)度。[...]如果你真的很傻很天真,你當(dāng)然可以定義一個(gè)有成百上千個(gè) case 的枚舉,在這種情況下,取決于最少所需要的比特?cái)?shù),枚舉可能占據(jù)兩個(gè)字節(jié)或者更多。
Swift 標(biāo)準(zhǔn)庫(kù)中的枚舉
在我們準(zhǔn)備繼續(xù)探索枚舉在項(xiàng)目中的不同用例之前,先看一下在 Swift 標(biāo)準(zhǔn)庫(kù)當(dāng)中是如何使用枚舉可能會(huì)更誘人,所以現(xiàn)在讓我們先來(lái)看看。
Bit 這個(gè)枚舉有兩個(gè)值,One 和 Zero。它被作為 CollectionOfOne<T> 中的 Index 類型。
FloatingPointClassification 這個(gè)枚舉定義了一系列 IEEE 754 可能的類別,比如 NegativeInfinity, PositiveZero 或 SignalingNaN。
Mirror.AncestorRepresentation 和 Mirror.DisplayStyle 這兩個(gè)枚舉被用在 Swift 反射 API 的上下文當(dāng)中。
Optional 這個(gè)就不用多說(shuō)了
Process 這個(gè)枚舉包含了當(dāng)前進(jìn)程的命令行參數(shù)(Process.argc, Process.arguments)。這是一個(gè)相當(dāng)有趣的枚舉類型,因?yàn)樵?Swift 1.0 當(dāng)中,它是被作為一個(gè)結(jié)構(gòu)體來(lái)實(shí)現(xiàn)的。
實(shí)踐用例
我們已經(jīng)在前面幾個(gè)小節(jié)當(dāng)中看過(guò)了許多有用的枚舉類型。包括 Optional,Either, FileNode 還有二叉樹。然而,還存在很多場(chǎng)合,使用枚舉要?jiǎng)龠^(guò)使用結(jié)構(gòu)體和類。一般來(lái)講,如果問題可以被分解為有限的不同類別,則使用枚舉應(yīng)該就是正確的選擇。即使只有兩種 case,這也是一個(gè)使用枚舉的完美場(chǎng)景,正如 Optional 和 Either 類型所展示的。
以下列舉了一些枚舉類型在實(shí)戰(zhàn)中的使用示例,可以用來(lái)點(diǎn)燃你的創(chuàng)造力。
錯(cuò)誤處理
說(shuō)到枚舉的實(shí)踐使用,當(dāng)然少不了在 Swift 2.0 當(dāng)中新推出的錯(cuò)誤處理。標(biāo)記為可拋出的函數(shù)可以拋出任何遵守了 ErrorType 空協(xié)議的類型。正如 Swift 官方文檔中所寫的:
Swift 的枚舉特別適用于構(gòu)建一組相關(guān)的錯(cuò)誤狀態(tài),可以通過(guò)關(guān)聯(lián)值來(lái)為其增加額外的附加信息。
作為一個(gè)示例,我們來(lái)看下流行的JSON解析框架 Argo。當(dāng) JSON 解析失敗的時(shí)候,它有可能是以下兩種主要原因:
JSON 數(shù)據(jù)缺少某些最終模型所需要的鍵(比如你的模型有一個(gè) username 的屬性,但是 JSON 中缺少了)
存在類型不匹配,比如說(shuō) username 需要的是 String 類型,而 JSON 中包含的是 NSNull6。
除此之外,Argo 還為不包含在上述兩個(gè)類別中的錯(cuò)誤提供了自定義錯(cuò)誤。它們的 ErrorType 枚舉是類似這樣的:
enum DecodeError: ErrorType {
case TypeMismatch(expected: String, actual: String)
case MissingKey(String)
case Custom(String)
}
所有的 case 都有一個(gè)關(guān)聯(lián)值用來(lái)包含關(guān)于錯(cuò)誤的附加信息。
一個(gè)更加通用的用于完整 HTTP / REST API 錯(cuò)誤處理的ErrorType應(yīng)該是類似這樣的:
enum APIError : ErrorType {
// Can't connect to the server (maybe offline?)
case ConnectionError(error: NSError)
// The server responded with a non 200 status code
case ServerError(statusCode: Int, error: NSError)
// We got no data (0 bytes) back from the server
case NoDataError
// The server response can't be converted from JSON to a Dictionary
case JSONSerializationError(error: ErrorType)
// The Argo decoding Failed
case JSONMappingError(converstionError: DecodeError)
}
這個(gè) ErrorType 實(shí)現(xiàn)了完整的 REST 程序棧解析有可能出現(xiàn)的錯(cuò)誤,包含了所有在解析結(jié)構(gòu)體與類時(shí)會(huì)出現(xiàn)的錯(cuò)誤。
如果你看得夠仔細(xì),會(huì)發(fā)現(xiàn)在JSONMappingError中,我們將Argo中的DecodeError封裝到了我們的APIError類型當(dāng)中,因?yàn)槲覀儠?huì)用 Argo 來(lái)作實(shí)際的 JSON 解析。
更多關(guān)于ErrorType以及此種枚舉類型的示例可以參看官方文檔。
觀察者模式
在 Swift 當(dāng)中,有許多方法來(lái)構(gòu)建觀察模式。如果使用 @objc 兼容標(biāo)記,則我們可以使用 NSNotificationCenter 或者 KVO。即使不用這個(gè)標(biāo)記,didSet語(yǔ)法也可以很容易地實(shí)現(xiàn)簡(jiǎn)單的觀察模式。在這里可以使用枚舉,它可以使被觀察者的變化更加清晰明了。設(shè)想我們要對(duì)一個(gè)集合進(jìn)行觀察。如果我們稍微思考一下就會(huì)發(fā)現(xiàn)這只有幾種可能的情況:一個(gè)或多個(gè)項(xiàng)被插入,一個(gè)或多個(gè)項(xiàng)被刪除,一個(gè)或多個(gè)項(xiàng)被更新。這聽起來(lái)就是枚舉可以完成的工作:
enum Change {
case Insertion(items: [Item])
case Deletion(items: [Item])
case Update(items: [Item])
}
之后,觀察對(duì)象就可以使用一個(gè)很簡(jiǎn)潔的方式來(lái)獲取已經(jīng)發(fā)生的事情的詳細(xì)信息。這也可以通過(guò)為其增加 oldValue 和 newValue 的簡(jiǎn)單方法來(lái)擴(kuò)展它的功能。
狀態(tài)碼
如果我們正在使用一個(gè)外部系統(tǒng),而這個(gè)系統(tǒng)使用了狀態(tài)碼(或者錯(cuò)誤碼)來(lái)傳遞錯(cuò)誤信息,類似 HTTP 狀態(tài)碼,這種情況下枚舉就是一種很明顯并且很好的方式來(lái)對(duì)信息進(jìn)行封裝7 。
enum HttpError: String {
case Code400 = "Bad Request"
case Code401 = "Unauthorized"
case Code402 = "Payment Required"
case Code403 = "Forbidden"
case Code404 = "Not Found"
}
結(jié)果類型映射(Map Result Types)
枚舉也經(jīng)常被用于將 JSON 解析后的結(jié)果映射成 Swift 的原生類型。這里有一個(gè)簡(jiǎn)短的例子:
enum JSON {
case JSONString(Swift.String)
case JSONNumber(Double)
case JSONObject([String : JSONValue])
case JSONArray([JSONValue])
case JSONBool(Bool)
case JSONNull
}
類似地,如果我們解析了其它的東西,也可以使用這種方式將解析結(jié)果轉(zhuǎn)化我們 Swift 的類型。
UIKit 標(biāo)識(shí)
枚舉可以用來(lái)將字符串類型的重用標(biāo)識(shí)或者 storyboard 標(biāo)識(shí)映射為類型系統(tǒng)可以進(jìn)行檢查的類型。假設(shè)我們有一個(gè)擁有很多原型 Cell 的 UITableView:
enum CellType: String {
case ButtonValueCell = "ButtonValueCell"
case UnitEditCell = "UnitEditCell"
case LabelCell = "LabelCell"
case ResultLabelCell = "ResultLabelCell"
}
單位
單位以及單位轉(zhuǎn)換是另一個(gè)使用枚舉的絕佳場(chǎng)合。可以將單位及其對(duì)應(yīng)的轉(zhuǎn)換率映射起來(lái),然后添加方法來(lái)對(duì)單位進(jìn)行自動(dòng)的轉(zhuǎn)換。以下是一個(gè)相當(dāng)簡(jiǎn)單的示例:
enum Liquid: Float {
case ml = 1.0
case l = 1000.0
func convert(amount amount: Float, to: Liquid) -> Float {
if self.rawValue < to.rawValue {
return (self.rawValue / to.rawValue) * amount
} else {
return (self.rawValue * to.rawValue) * amount
}
}
}
// Convert liters to milliliters
print (Liquid.l.convert(amount: 5, to: Liquid.ml))
另一個(gè)示例是貨幣的轉(zhuǎn)換。以及數(shù)學(xué)符號(hào)(比如角度與弧度)也可以從中受益。
游戲
游戲也是枚舉中的另一個(gè)相當(dāng)好的用例,屏幕上的大多數(shù)實(shí)體都屬于一個(gè)特定種族的類型(敵人,障礙,紋理,...)。相對(duì)于本地的 iOS 或者 Mac 應(yīng)用,游戲更像是一個(gè)白板。即開發(fā)游戲我們可以使用全新的對(duì)象以及全新的關(guān)聯(lián)創(chuàng)造一個(gè)全新的世界,而 iOS 或者 OSX 需要使用預(yù)定義的 UIButtons,UITableViews,UITableViewCells 或者 NSStackView.
不僅如此,由于枚舉可以遵守協(xié)議,我們可以利用協(xié)議擴(kuò)展和基于協(xié)議的編程為不同為游戲定義的枚舉增加功能。這里是一個(gè)用來(lái)展示這種層級(jí)的的簡(jiǎn)短示例:
enum FlyingBeast { case Dragon, Hippogriff, Gargoyle }
enum Horde { case Ork, Troll }
enum Player { case Mage, Warrior, Barbarian }
enum NPC { case Vendor, Blacksmith }
enum Element { case Tree, Fence, Stone }
protocol Hurtable {}
protocol Killable {}
protocol Flying {}
protocol Attacking {}
protocol Obstacle {}
extension FlyingBeast: Hurtable, Killable, Flying, Attacking {}
extension Horde: Hurtable, Killable, Attacking {}
extension Player: Hurtable, Obstacle {}
extension NPC: Hurtable {}
extension Element: Obstacle {}
字符串類型化
在一個(gè)稍微大一點(diǎn)的 Xcode 項(xiàng)目中,我們很快就會(huì)有一大堆通過(guò)字符串來(lái)訪問的資源。在前面的小節(jié)中,我們已經(jīng)提過(guò)重用標(biāo)識(shí)和 storyboard 的標(biāo)識(shí),但是除了這兩樣,還存在很多資源:圖像,Segues,Nibs,字體以及其它資源。通常情況下,這些資源都可以分成不同的集合。如果是這樣的話,一個(gè)類型化的字符串會(huì)是一個(gè)讓編譯器幫我們進(jìn)行類型檢查的好方法。
enum DetailViewImages: String {
case Background = "bg1.png"
case Sidebar = "sbg.png"
case ActionButton1 = "btn1_1.png"
case ActionButton2 = "btn2_1.png"
}
對(duì)于 iOS 開發(fā)者,R.swift這個(gè)第三方庫(kù)可以為以上提到的情況自動(dòng)生成結(jié)構(gòu)體。但是有些時(shí)候你可能需要有更多的控制(或者你可能是一個(gè)Mac開發(fā)者8)。
API 端點(diǎn)
Rest API 是枚舉的絕佳用例。它們都是分組的,它們都是有限的 API 集合,并且它們也可能會(huì)有附加的查詢或者命名的參數(shù),而這可以使用關(guān)聯(lián)值來(lái)實(shí)現(xiàn)。
這里有個(gè) Instagram API 的簡(jiǎn)化版:
enum Instagram {
enum Media {
case Popular
case Shortcode(id: String)
case Search(lat: Float, min_timestamp: Int, lng: Float, max_timestamp: Int, distance: Int)
}
enum Users {
case User(id: String)
case Feed
case Recent(id: String)
}
}
Ash Furrow的Moya框架就是基本這個(gè)思想,使用枚舉對(duì) rest 端點(diǎn)進(jìn)行映射。
鏈表
Airspeed Velocity有一篇極好的文章說(shuō)明了如何使用枚舉來(lái)實(shí)現(xiàn)一個(gè)鏈表。那篇文章中的大多數(shù)代碼都超出了枚舉的知識(shí),并涉及到了大量其它有趣的主題9,但是,鏈表最基本的定義是類似這樣的(我對(duì)其進(jìn)行了一些簡(jiǎn)化):
enum List {
case End
indirect case Node(Int, next: List)
}
每一個(gè)節(jié)點(diǎn)(Node) case 都指向了下一個(gè) case, 通過(guò)使用枚舉而非其它類型,我們可以避免使用一個(gè)可選的 next 類型以用來(lái)表示鏈表的結(jié)束。
Airspeed Velocity 還寫過(guò)一篇超贊的博客,關(guān)于如何使用 Swift 的間接枚舉類型來(lái)實(shí)現(xiàn)紅黑樹,所以如果你已經(jīng)閱讀過(guò)關(guān)于鏈表的博客,你可能想繼續(xù)閱讀這篇關(guān)于紅黑樹的博客。
設(shè)置字典(Setting Dictionaries)
這是 Erica Sadun 提出的非常非常機(jī)智的解決方案。簡(jiǎn)單來(lái)講,就是任何我們需要用一個(gè)屬性的字典來(lái)對(duì)一個(gè)項(xiàng)進(jìn)行設(shè)置的時(shí)候,都應(yīng)該使用一系列有關(guān)聯(lián)值的枚舉來(lái)替代。使用這方法,類型檢查系統(tǒng)可以確保配置的值都是正確的類型。
關(guān)于更多的細(xì)節(jié),以及合適的例子,可以閱讀下她的文章。
局限
與之前類似,我將會(huì)用一系列枚舉的局限性來(lái)結(jié)束本篇文章。
提取關(guān)聯(lián)值
David Owens寫過(guò)一篇文章,他覺得當(dāng)前的關(guān)聯(lián)值提取方式是很笨重的。我墻裂推薦你去看一下他的原文,在這里我對(duì)它的要旨進(jìn)行下說(shuō)明:為了從一個(gè)枚舉中獲取關(guān)聯(lián)值,我們必須使用模式匹配。然而,關(guān)聯(lián)值就是關(guān)聯(lián)在特定枚舉 case 的高效元組。而元組是可以使用更簡(jiǎn)單的方式來(lái)獲取它內(nèi)部值,即 .keyword 或者 .0。
// Enums
enum Ex { case Mode(ab: Int, cd: Int) }
if case Ex.Mode(let ab, let cd) = Ex.Mode(ab: 4, cd: 5) {
print(ab)
}
// vs tuples:
let tp = (ab: 4, cd: 5)
print(tp.ab)
如果你也同樣覺得我們應(yīng)該使用相同的方法來(lái)對(duì)枚舉進(jìn)行解構(gòu)(deconstruct),這里有個(gè) rdar: rdar://22704262 (譯者注:一開始我不明白 rdar 是啥意思,后來(lái)我 google 了下,如果你也有興趣,也可以自己去搜索一下)
相等性
擁有關(guān)聯(lián)值的枚舉沒有遵守 equatable 協(xié)議。這是一個(gè)遺憾,因?yàn)樗鼮楹芏嗍虑樵黾恿瞬槐匾膹?fù)雜和麻煩。深層的原因可能是因?yàn)殛P(guān)聯(lián)值的底層使用是使用了元組,而元組并沒有遵守 equatable 協(xié)議。然而,對(duì)于限定的 case 子集,如果這些關(guān)聯(lián)值的類型都遵守了 equatable 類型,我認(rèn)為編譯器應(yīng)該默認(rèn)為其生成 equatable 擴(kuò)展。
// Int 和 String 是可判等的, 所以 Mode 應(yīng)該也是可判等的
enum Ex { case Mode(ab: Int, cd: String) }
// Swift 應(yīng)該能夠自動(dòng)生成這個(gè)函數(shù)
func == (lhs: Ex.Mode, rhs: Ex.Mode) -> Bool {
switch (lhs, rhs) {
case (.Mode(let a, let b), .Mode(let c, let d)):
return a == c && b == d
default:
return false
}
}
元組(Tuples)
最大的問題就是對(duì)元組的支持。我喜歡使用元組,它們可以使很多事情變得更簡(jiǎn)單,但是他們目前還處于無(wú)文檔狀態(tài)并且在很多場(chǎng)合都無(wú)法使用。在枚舉當(dāng)中,我們無(wú)法使用元組作為枚舉的值:
enum Devices: (intro: Int, name: String) {
case iPhone = (intro: 2007, name: "iPhone")
case AppleTV = (intro: 2006, name: "Apple TV")
case AppleWatch = (intro: 2014, name: "Apple Watch")
}
這似乎看起來(lái)并不是一個(gè)最好的示例,但是我們一旦開始使用枚舉,就會(huì)經(jīng)常陷入到需要用到類似上面這個(gè)示例的情形中。
迭代枚舉的所有case
這個(gè)我們已經(jīng)在前面討論過(guò)了。目前還沒有一個(gè)很好的方法來(lái)獲得枚舉中的所有 case 的集合以使我們可以對(duì)其進(jìn)行迭代。
默認(rèn)關(guān)聯(lián)值
另一個(gè)會(huì)碰到的事是枚舉的關(guān)聯(lián)值總是類型,但是我們卻無(wú)法為這些類型指定默認(rèn)值。假設(shè)有這樣一種情況:
enum Characters {
case Mage(health: Int = 70, magic: Int = 100, strength: Int = 30)
case Warrior(health: Int = 100, magic: Int = 0, strength: Int = 100)
case Neophyte(health: Int = 50, magic: Int = 20, strength: Int = 80)
}
我們依然可以使用不同的值創(chuàng)建新的 case,但是角色的默認(rèn)設(shè)置依然會(huì)被映射。
變化
10/26/2015
增加局限性示例(相等性 & 獲取關(guān)聯(lián)值)
增加 Erica Sadun 的關(guān)聯(lián)枚舉示例
10/22/2015
合并來(lái)自 #6 @mabidakun的PR
增加枚舉底層的鏈接
將帳號(hào)示例拆分為兩個(gè)更容易理解的片段。
10/21/2015
合并來(lái)自 #4 @blixt和#2 @kandelvijayavolare和#3 @sriniram以及#5 @SixFiveSoftware的PR
為帳號(hào)示例添加調(diào)用代碼
增加 ErrorType 示例
解釋
1、可以使用一些小技術(shù)來(lái)達(dá)到這個(gè)目的,具體的請(qǐng)參照下面的文章內(nèi)容
2、為了演示的緣故,這個(gè)示例的實(shí)現(xiàn)經(jīng)過(guò)的簡(jiǎn)化。在真實(shí)的開發(fā)中,應(yīng)該使用可選類型以及反向順序的參數(shù)。可以參考一下現(xiàn)在十分流行的函數(shù)式編程庫(kù),如 Swiftz 和 Dollar
3、這個(gè)示例直接采用了Swift 官方文檔的示例
4、經(jīng)常使得他們定義的位置很難被發(fā)現(xiàn)
5、這是一個(gè)簡(jiǎn)化版的,當(dāng)然,Swift 為我們加了很多的語(yǔ)法糖
6、如果你在應(yīng)用中使用過(guò) JSON,應(yīng)該也曾經(jīng)碰到過(guò)這個(gè)問題
7、順便一提,不能直接使用數(shù)字做為枚舉 case 的名稱,因此直接使用 400 是不行的
8、雖然如此,不過(guò)支持 Mac 版的 R.swift 好像就快推出了
9、這句話可以解釋為: 打開鏈接,并開始閱讀文章