Swift 中枚舉高級用法及實踐

做者:Benedikt Terhechte,原文連接,原文日期:2015-10-17
譯者:小鍋pmst;校對:shanks;定稿:shankshtml

譯者注:做爲一個走心且有逼格的翻譯組,咱們對本篇文章中的代碼都進行了驗證,而且寫了將代碼分爲上下兩篇作成了 playground,代碼中有詳盡的註釋。能夠到這個github地址上進行下載,這個代碼由翻譯組的另外一位小夥伴 ppt 提供。node

本文是一篇詳細且具備實戰意義的教程,涵蓋幾乎全部枚舉(Enum)知識點,爲你解答Swift中枚舉的應用場合以及使用方法。ios

switch語句相似,Swift中的枚舉乍看之下更像是C語言中枚舉的進階版本,即容許你定義一種類型,用於表示普通事情中某種用例。不過深刻挖掘以後,憑藉Swift背後特別的設計理念,相比較C語言枚舉來講其在實際場景中的應用更爲普遍。特別是做爲強大的工具,Swift中的枚舉可以清晰表達代碼的意圖。git

本文中,咱們將首先了解基礎語法和使用枚舉的可能性,接着經過實戰教你如何以及什麼時候使用枚舉。最後咱們還會大體瞭解下Swift標準庫中枚舉是如何被使用的。github

正式開始學習以前,先給出枚舉的定義。以後咱們將回過頭再來討論它。編程

枚舉聲明的類型是囊括可能狀態的有限集,且能夠具備附加值。經過內嵌(nesting),方法(method),關聯值(associated values)和模式匹配(pattern matching),枚舉能夠分層次地定義任何有組織的數據。swift

深刻理解(Diving In)

簡要概述如何定義和使用枚舉。數組

定義基本的枚舉類型(Defining Basic Enums)

試想咱們正在開發一款遊戲,玩家可以朝四個方向移動。因此嘍,玩家的運動軌跡受到了限制。顯然,咱們可以使用枚舉來表述這一狀況:數據結構

enum Movement{
    case Left
    case Right
    case Top
    case Bottom
}

緊接着,你可使用多種模式匹配結構獲取到Movement的枚舉值,或者按照特定狀況執行操做:app

let aMovement = Movement.Left

// switch 分狀況處理
switch aMovement{
case .Left: print("left")
default:()
}

// 明確的case狀況
if case .Left = aMovement{
    print("left")
}

if aMovement == .Left { print("left") }

案例中,咱們無須明確指出enum的實際名稱(即case Move.Left:print("Left"))。由於類型檢查器可以自動爲此進行類型推算。這對於那些UIKit以及AppKit中錯綜複雜的枚舉是灰常有用的。

枚舉值(Enum Values)

固然,你可能想要爲enum中每一個case分配一個值。這至關有用,好比枚舉自身實際與某事或某物掛鉤時,每每這些東西又須要使用不一樣類型來表述。在C語言中,你只能爲枚舉case分配整型值,而Swift則提供了更多的靈活性。

// 映射到整型
enum Movement: Int {
    case Left = 0
    case Right = 1
    case Top = 2
    case Bottom = 3
}

// 一樣你能夠與字符串一一對應
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均可以(同時注意枚舉中的花式unicode)
enum Constants: Double {
    case π = 3.14159
    case e = 2.71828
    case φ = 1.61803398874
    case λ = 1.30357
}

對於StringInt類型來講,你甚至能夠忽略爲枚舉中的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"
// 譯者注: 這個是swift2.0新增語法
enum CompassPoint: String {
    case North, South, East, West
}

Swift枚舉中支持如下四種關聯值類型:

  • 整型(Integer)

  • 浮點數(Float Point)

  • 字符串(String)

  • 布爾類型(Boolean)

所以你沒法1爲枚舉分配諸如CGPoint類型的值。

假若你想要讀取枚舉的值,能夠經過rawValue屬性來實現:

let bestHouse = House.Stark
print(bestHouse.rawValue)
// prints "Winter is coming"

不過某種情形下,你可能想要經過一個已有的raw value來建立一個enum case。這種狀況下,枚舉提供了一個指定構造方法:

enum Movement: Int {
    case Left = 0
    case Right = 1
    case Top = 2
    case Bottom = 3
}
// 建立一個movement.Right 用例,其raw value值爲1
let rightMovement = Movement(rawValue: 1)

假若使用rawValue構造器,切記它是一個可失敗構造器(failable initializer)。換言之,構造方法返回值爲可選類型值,由於有時候傳入的值可能與任意一個case都不匹配。好比Movement(rawValue:42)

若是你想要以底層 C 二進制編碼形式呈現某物或某事,使得更具可讀性,這是一個很是有用的功能。例如,能夠看一下BSD kqeue library中的VNode Flags標誌位的編碼方式:

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
}

如此即可以使你的DeleteWrite用例聲明一目瞭然,稍後一旦須要,只需將raw value傳入 C 函數中便可。

嵌套枚舉(Nesting Enums)

若是你有特定子類型的需求,能夠對enum進行嵌套。這樣就容許你爲實際的enum中包含其餘明確信息的enum。以RPG遊戲中的每一個角色爲例,每一個角色可以擁有武器,所以全部角色均可以獲取同一個武器集合。而遊戲中的其餘實例則沒法獲取這些武器(好比食人魔,它們僅使用棍棒)。

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
}

如今,你能夠經過層級結構來描述角色容許訪問的項目條。

let character = Character.Thief
let weapon = Character.Weapon.Bow
let helmet = Character.Helmet.Iron

包含枚舉(Containing Enums)

一樣地,你也可以在structsclasses中內嵌枚舉。接着上面的例子:

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)

一樣地,這也將有助於咱們將相關的信息集中在一個位置。

關聯值(Associated Value)

關聯值是將額外信息附加到enum case中的一種極好的方式。打個比方,你正在開發一款交易引擎,可能存在兩種不一樣的交易類型。除此以外每手交易還要制定明確的股票名稱和交易數量:

簡單例程(Simple Example)

enum Trade {
    case Buy
    case Sell
}
func trade(tradeType: Trade, stock: String, amount: Int) {}

然而股票的價值和數量顯然從屬於交易,讓他們做爲獨立的參數顯得模棱兩可。你可能已經想到要往struct中內嵌一個枚舉了,不過關聯值提供了一種更清爽的解決方案:

enum Trade {
    case Buy(stock: String, amount: Int)
    case Sell(stock: String, amount: Int)
}
func trade(type: Trade) {}

模式匹配(Pattern Mathching)

若是你想要訪問這些值,模式匹配再次救場:

let trade = Trade.Buy(stock: "APPL", amount: 500)
if case let Trade.Buy(stock, amount) = trade {
    print("buy \(amount) of \(stock)")
}

標籤(Labels)

關聯值不須要附加標籤的聲明:

enum Trade {
   case Buy(String, Int)
   case Sell(String, Int)
}

假若你添加了,那麼,每當建立枚舉用例時,你都須要將這些標籤標示出來。

元組參數(Tuple as Arguments)

更重要的是,Swift內部相關信息實際上是一個元組,因此你能夠像下面這樣作:

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"

語法容許您將元組看成一個簡單的數據結構,稍後元組將自動轉換到高級類型,就好比enum case。想象一個應用程序可讓用戶來配置電腦:

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))))

配置的每一個步驟均經過遞交元組到enum中進行內容更新。假若咱們從函數式編程2中得到啓發,這將變得更好。

infix operator <^> { associativity left }

func <^>(a: Config, f: (Config) -> Config) -> Config { 
    return f(a)
}

最後,咱們能夠將不一樣配置步驟串聯起來。這在配置步驟繁多的狀況下至關有用。

let config = (0, "", "") <^> selectRAM  <^> selectCPU <^> selectGPU
let aCube = Desktop.Cube(config)

使用案例(Use Case Example)

關聯值能夠以多種方式使用。常言道:一碼勝千言, 下面就上幾段簡單的示例代碼,這幾段代碼沒有特定的順序。

// 擁有不一樣值的用例
enum UserAction {
  case OpenURL(url: NSURL)
  case SwitchProcess(processId: UInt32)
  case Restart(time: NSDate?, intoCommandLine: Bool)
}

// 假設你在實現一個功能強大的編輯器,這個編輯器容許多重選擇,
// 正如 Sublime Text : https://www.youtube.com/watch?v=i2SVJa2EGIw
enum Selection {
  case None
  case Single(Range<Int>)
  case Multiple([Range<Int>])
}

// 或者映射不一樣的標識碼
enum Barcode {
    case UPCA(numberSystem: Int, manufacturer: Int, product: Int, check: Int)
    case QRCode(productCode: String)
}

// 又或者假設你在封裝一個 C 語言庫,正如 Kqeue BSD/Darwin 通知系統:
// 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)
}

// 最後, 一個 RPG 遊戲中的全部可穿戴裝備可使用一個枚舉來進行映射,
// 能夠爲一個裝備增長重量和持久兩個屬性
// 如今能夠僅用一行代碼來增長一個"鑽石"屬性,如此一來咱們即可以增長几件新的鑲嵌鑽石的可穿戴裝備
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)"

枚舉中的方法爲每個enum case而「生」。因此假若想要在特定狀況執行特定代碼的話,你須要分支處理或採用switch語句來明確正確的代碼路徑。

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)

儘管增長一個存儲屬性到枚舉中不被容許,但你依然可以建立計算屬性。固然,計算屬性的內容都是創建在枚舉值下或者枚舉關聯值獲得的。

enum Device {
  case iPad, iPhone
  var year: Int {
    switch self {
        case iPhone: return 2007
        case iPad: return 2010
     }
  }
}

靜態方法(Static Methods)

你也可以爲枚舉建立一些靜態方法(static methods)。換言之經過一個非枚舉類型來建立一個枚舉。在這個示例中,咱們須要考慮用戶有時將蘋果設備叫錯的狀況(好比AppleWatch叫成iWatch),須要返回一個合適的名稱。

enum Device { 
    case AppleWatch 
    static func fromSlang(term: String) -> Device? {
      if term == "iWatch" {
      return .AppleWatch
      }
      return nil
    }
}
print (Device.fromSlang("iWatch"))

可變方法(Mutating Methods)

方法能夠聲明爲mutating。這樣就容許改變隱藏參數selfcase值了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 如今等於.On
ovenLight.next()
// ovenLight 如今等於.Off

小結(To Recap)

至此,咱們已經大體瞭解了Swift中枚舉語法的基本用例。在開始邁向進階之路以前,讓咱們從新審視文章開篇給出的定義,看看如今是否變得更清晰了。

枚舉聲明的類型是囊括可能狀態的有限集,且能夠具備附加值。經過內嵌(nesting),方法(method),關聯值(associated values)和模式匹配(pattern matching),枚舉能夠分層次地定義任何有組織的數據。

如今咱們已經對這個定義更加清晰了。確實,若是咱們添加關聯值和嵌套,enum就看起來就像一個封閉的、簡化的struct。相比較struct,前者優點體如今可以爲分類與層次結構編碼。

// 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)
}

方法和靜態方法的添加容許咱們爲enum附加功能,這意味着無須依靠額外函數就能實現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()
}

枚舉進階(Advanced Enum Usage)

協議(Protocols)

我已經說起了structsenums之間的類似性。除了附加方法的能力以外,Swift也容許你在枚舉中使用協議(Protocols)協議擴展(Protocol Extension)

Swift協議定義一個接口或類型以供其餘數據結構來遵循。enum固然也不例外。咱們先從Swift標準庫中的一個例子開始.

CustomStringConvertible是一個以打印爲目的的自定義格式化輸出的類型。

protocol CustomStringConvertible {
  var description: String { get }
}

該協議只有一個要求,即一個只讀(getter)類型的字符串(String類型)。咱們能夠很容易爲enum實現這個協議。

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

一些協議的實現可能須要根據內部狀態來相應處理要求。例如定義一個管理銀行帳號的協議。

protocol AccountCompatible {
  var remainingFunds: Int { get }
  mutating func addFunds(amount: Int) throws
  mutating func removeFunds(amount: Int) throws
}

你也許會簡單地拿struct實現這個協議,可是考慮應用的上下文,enum是一個更明智的處理方法。不過你沒法添加一個存儲屬性到enum中,就像var remainingFuns:Int。那麼你會如何構造呢?答案灰常簡單,你可使用關聯值完美解決:

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的協議擴展(protocl extension)中定義必須的協議函數:

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

正如你所看見的,咱們經過將值存儲到enum cases中實現了協議全部要求項。如此作法還有一個妙趣橫生的地方:如今整個代碼基礎上你只須要一個模式匹配就能測試空帳號輸入的狀況。你不須要關心剩餘資金是否等於零。

同時,咱們也在帳號(Accout)中內嵌了一個遵循ErrorType協議的枚舉,這樣咱們就可使用Swift2.0語法來進行錯誤處理了。這裏給出更詳細的使用案例教程。

擴展(Extensions)

正如剛纔所見,枚舉也能夠進行擴展。最明顯的用例就是將枚舉的casemethod分離,這樣閱讀你的代碼可以簡單快速地消化掉enum內容,緊接着轉移到方法定義:

enum Entities {
    case Soldier(x: Int, y: Int)
    case Tank(x: Int, y: Int)
    case Player(x: Int, y: Int)
}

如今,咱們爲enum擴展方法:

extension Entities {
   mutating func move(dist: CGVector) {}
   mutating func attack() {}
}

你一樣能夠經過寫一個擴展來遵循一個特定的協議:

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)

枚舉也支持泛型參數定義。你可使用它們以適應枚舉中的關聯值。就拿直接來自Swift標準庫中的簡單例子來講,即Optional類型。你主要可能經過如下幾種方式使用它:可選鏈(optional chaining(?))、if-let可選綁定、guard let、或switch,可是從語法角度來講你也能夠這麼使用Optional:

let aValue = Optional<Int>.Some(5)
let noValue = Optional<Int>.None
if noValue == Optional.None { print("No value") }

這是Optional最直接的用例,並未使用任何語法糖,可是不能否認Swift中語法糖的加入使得你的工做更簡單。若是你觀察上面的實例代碼,你恐怕已經猜到Optional內部實現是這樣的5:

// Simplified implementation of Swift's Optional
enum MyOptional<T> {
  case Some(T)
  case None
}

這裏有啥特別呢?注意枚舉的關聯值採用泛型參數T做爲自身類型,這樣可選類型構造任何你想要的返回值。

枚舉能夠擁有多個泛型參數。就拿熟知的Either類爲例,它並不是是Swift標準庫中的一部分,而是實現於衆多開源庫以及
其餘函數式編程語言,好比HaskellF#。設計想法是這樣的:相比較僅僅返回一個值或沒有值(née Optional),你更指望返回一個成功值或者一些反饋信息(好比錯誤值)。

// 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中全部在classstruct中奏效的類型約束,在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 新增的一個類型。 它們容許將枚舉中一個 case 的關聯值再次定義爲枚舉。舉個例子,假設咱們想定義一個文件系統,用來表示文件以及包含文件的目錄。若是將文件目錄定義爲枚舉的 case,則目錄 case 的關聯值應該再包含一個文件的數組做爲它的關聯值。由於這是一個遞歸的操做,編譯器須要對此進行一個特殊的準備。Swift 文檔中是這麼寫的:

枚舉和 case 能夠被標記爲間接的(indrect),這意味它們的關聯值是被間接保存的,這容許咱們定義遞歸的數據結構。

因此,若是咱們要定義 FileNode 的枚舉,它應該會是這樣的:

enum FileNode {
  case File(name: String)
  indirect case Folder(name: String, files: [FileNode])
}

此處的 indrect 關鍵字告訴編譯器間接地處理這個枚舉的 case。也能夠對整個枚舉類型使用這個關鍵字。做爲例子,咱們來定義一個二叉樹:

indirect enum Tree<Element: Comparable> {
    case Empty
    case Node(Tree<Element>,Element,Tree<Element>)
}

這是一個很強大的特性,可讓咱們用很是簡潔的方式來定義一個有着複雜關聯的數據結構。

使用自定義類型做爲枚舉的值

若是咱們忽略關聯值,則枚舉的值就只能是整型,浮點型,字符串和布爾類型。若是想要支持別的類型,則能夠經過實現 StringLiteralConvertible 協議來完成,這可讓咱們經過對字符串的序列化和反序列化來使枚舉支持自定義類型。

做爲一個例子,假設咱們要定義一個枚舉來保存不一樣的 iOS 設備的屏幕尺寸:

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)
}

然而,這段代碼不能經過編譯。由於 CGPoint 並非一個常量,不能用來定義枚舉的值。咱們須要爲想要支持的自定義類型增長一個擴展,讓其實現 StringLiteralConvertible 協議。這個協議要求咱們實現三個構造方法,這三個方法都須要使用一個String類型的參數,而且咱們須要將這個字符串轉換成咱們須要的類型(此處是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)
    }
}

如今就能夠來實現咱們須要的枚舉了,不過這裏有一個缺點:初始化的值必須寫成字符串形式,由於這就是咱們定義的枚舉須要接受的類型(記住,咱們實現了 StringLiteralConvertible,所以String能夠轉化成CGSize類型)

enum Devices: CGSize {
   case iPhone3GS = "{320, 480}"
   case iPhone5 = "{320, 568}"
   case iPhone6 = "{375, 667}"
   case iPhone6Plus = "{414, 736}"
}

終於,咱們可使用 CGPoint 類型的枚舉了。須要注意的是,當要獲取真實的 CGPoint 的值的時候,咱們須要訪問枚舉的是 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

使用字符串序列化的形式,會讓使用自定義類型的枚舉比較困難,然而在某些特定的狀況下,這也會給咱們增長很多便利(比較使用NSColor / UIColor的時候)。不只如此,咱們徹底能夠對本身定義的類型使用這個方法。

對枚舉的關聯值進行比較

在一般狀況下,枚舉是很容易進行相等性判斷的。一個簡單的 enum T { case a, b } 實現默認支持相等性判斷 T.a == T.b, T.b != T.a

然而,一旦咱們爲枚舉增長了關聯值,Swift 就沒有辦法正確地爲兩個枚舉進行相等性判斷,須要咱們本身實現 == 運行符。這並非很困難:

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
   }
}

正如咱們所見,咱們經過 switch 語句對兩個枚舉的 case 進行判斷,而且只有當它們的 case 是匹配的時候(好比 Buy 和 Buy)纔對它們的真實關聯值進行判斷。

自定義構造方法

靜態方法 一節當中咱們已經提到它們能夠做爲從不一樣數據構造枚舉的方便形式。在以前的例子裏也展現過,對出版社常常誤用的蘋果設備名返回正確的名字:

enum Device { 
    case AppleWatch 
    static func fromSlang(term: String) -> Device? {
      if term == "iWatch" {
      return .AppleWatch
      }
      return nil
    }
}

咱們也可使用自定義構造方法來替換靜態方法。枚舉與結構體和類的構造方法最大的不一樣在於,枚舉的構造方法須要將隱式的 self 屬性設置爲正確的 case。

enum Device { 
    case AppleWatch 
    init?(term: String) {
      if term == "iWatch" {
      self = .AppleWatch
      }
      return nil
    }
}

在這個例子中,咱們使用了可失敗(failable)的構造方法。可是,普通的構造方法也能夠工做得很好:

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"

對枚舉的 case 進行迭代

一個特別常常被問到的問題就是如何對枚舉中的 case 進行迭代。惋惜的是,枚舉並無遵照SequenceType協議,所以沒有一個官方的作法來對其進行迭代。取決於枚舉的類型,對其進行迭代可能也簡單,也有可能很困難。在StackOverflow上有一個很好的討論貼。貼子裏面討論到的不一樣狀況太多了,若是隻在這裏摘取一些會有片面性,而若是將所有狀況都列出來,則會太多。

對 Objective-C 的支持

基於整型的枚舉,如 enum Bit: Int { case Zero = 0; case One = 1 } 能夠經過 @objc 標識來將其橋接到 Objective-C 當中。然而,一旦使用整型以外的類型(如 String)或者開始使用關聯值,咱們就沒法在 Objective-C 當中使用這些枚舉了。

有一個名爲_ObjectiveCBridgeable的隱藏協議,可讓規範咱們以定義合適的方法,如此一來,Swift 即可以正確地將枚舉轉成 Objective-C 類型,但我猜這個協議被隱藏起來必定是有緣由的。然而,從理論上來說,這個協議仍是容許咱們將枚舉(包括其實枚舉值)正確地橋接到 Objective-C 當中。

可是,咱們並不必定非要使用上面提到的這個方法。爲枚舉添加兩個方法,使用 @objc 定義一個替代類型,如此一來咱們即可以自由地將枚舉進行轉換了,而且這種方式不須要遵照私有協議:

enum Trade {
    case Buy(stock: String, amount: Int)
    case Sell(stock: String, amount: Int)
}

// 這個類型也能夠定義在 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
    }
    }
}

這個方法有一個的缺點,咱們須要將枚舉映射爲 Objective-C 中的 NSObject 基礎類型(咱們也能夠直接使用 NSDictionary),可是,當咱們碰到一些確實須要在 Objective-C 當中獲取有關聯值的枚舉時,這是一個可使用的方法。

枚舉底層

Erica Sadun 寫過一篇很流弊的關於枚舉底層的博客,涉及到枚舉底層的方方面面。在生產代碼中毫不應該使用到這些東西,可是學習一下仍是至關有趣的。在這裏,我準備只提到那篇博客中一條,若是想了解更多,請移步到原文:

枚舉一般都是一個字節長度。[...]若是你真的很傻很天真,你固然能夠定義一個有成百上千個 case 的枚舉,在這種狀況下,取決於最少所須要的比特數,枚舉可能佔據兩個字節或者更多。

Swift 標準庫中的枚舉

在咱們準備繼續探索枚舉在項目中的不一樣用例以前,先看一下在 Swift 標準庫當中是如何使用枚舉可能會更誘人,因此如今讓咱們先來看看。

  • Bit 這個枚舉有兩個值,OneZero。它被做爲 CollectionOfOne<T> 中的 Index 類型。

  • FloatingPointClassification 這個枚舉定義了一系列 IEEE 754 可能的類別,好比 NegativeInfinity, PositiveZeroSignalingNaN

  • Mirror.AncestorRepresentationMirror.DisplayStyle 這兩個枚舉被用在 Swift 反射 API 的上下文當中。

  • Optional 這個就不用多說了

  • Process 這個枚舉包含了當前進程的命令行參數(Process.argc, Process.arguments)。這是一個至關有趣的枚舉類型,由於在 Swift 1.0 當中,它是被做爲一個結構體來實現的。

實踐用例

咱們已經在前面幾個小節當中看過了許多有用的枚舉類型。包括 OptionalEither, FileNode 還有二叉樹。然而,還存在不少場合,使用枚舉要賽過使用結構體和類。通常來說,若是問題能夠被分解爲有限的不一樣類別,則使用枚舉應該就是正確的選擇。即便只有兩種 case,這也是一個使用枚舉的完美場景,正如 Optional 和 Either 類型所展現的。

如下列舉了一些枚舉類型在實戰中的使用示例,能夠用來點燃你的創造力。

錯誤處理

說到枚舉的實踐使用,固然少不了在 Swift 2.0 當中新推出的錯誤處理。標記爲可拋出的函數能夠拋出任何遵照了 ErrorType 空協議的類型。正如 Swift 官方文檔中所寫的:

Swift 的枚舉特別適用於構建一組相關的錯誤狀態,能夠經過關聯值來爲其增長額外的附加信息。

做爲一個示例,咱們來看下流行的JSON解析框架 Argo。當 JSON 解析失敗的時候,它有多是如下兩種主要緣由:

  1. JSON 數據缺乏某些最終模型所須要的鍵(好比你的模型有一個 username 的屬性,可是 JSON 中缺乏了)

  2. 存在類型不匹配,好比說 username 須要的是 String 類型,而 JSON 中包含的是 NSNull6

除此以外,Argo 還爲不包含在上述兩個類別中的錯誤提供了自定義錯誤。它們的 ErrorType 枚舉是相似這樣的:

enum DecodeError: ErrorType {
  case TypeMismatch(expected: String, actual: String)
  case MissingKey(String)
  case Custom(String)
}

全部的 case 都有一個關聯值用來包含關於錯誤的附加信息。

一個更加通用的用於完整 HTTP / REST API 錯誤處理的ErrorType應該是相似這樣的:

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)
}

這個 ErrorType 實現了完整的 REST 程序棧解析有可能出現的錯誤,包含了全部在解析結構體與類時會出現的錯誤。

若是你看得夠仔細,會發如今JSONMappingError中,咱們將Argo中的DecodeError封裝到了咱們的APIError類型當中,由於咱們會用 Argo 來做實際的 JSON 解析。

更多關於ErrorType以及此種枚舉類型的示例能夠參看官方文檔

觀察者模式

在 Swift 當中,有許多方法來構建觀察模式。若是使用 @objc 兼容標記,則咱們可使用 NSNotificationCenter 或者 KVO。即便不用這個標記,didSet語法也能夠很容易地實現簡單的觀察模式。在這裏可使用枚舉,它可使被觀察者的變化更加清晰明瞭。設想咱們要對一個集合進行觀察。若是咱們稍微思考一下就會發現這隻有幾種可能的狀況:一個或多個項被插入,一個或多個項被刪除,一個或多個項被更新。這聽起來就是枚舉能夠完成的工做:

enum Change {
     case Insertion(items: [Item])
     case Deletion(items: [Item])
     case Update(items: [Item])
}

以後,觀察對象就可使用一個很簡潔的方式來獲取已經發生的事情的詳細信息。這也能夠經過爲其增長 oldValuenewValue 的簡單方法來擴展它的功能。

狀態碼

若是咱們正在使用一個外部系統,而這個系統使用了狀態碼(或者錯誤碼)來傳遞錯誤信息,相似 HTTP 狀態碼,這種狀況下枚舉就是一種很明顯而且很好的方式來對信息進行封裝7

enum HttpError: String {
  case Code400 = "Bad Request"
  case Code401 = "Unauthorized"
  case Code402 = "Payment Required"
  case Code403 = "Forbidden"
  case Code404 = "Not Found"
}

結果類型映射(Map Result Types)

枚舉也常常被用於將 JSON 解析後的結果映射成 Swift 的原生類型。這裏有一個簡短的例子:

enum JSON {
    case JSONString(Swift.String)
    case JSONNumber(Double)
    case JSONObject([String : JSONValue])
    case JSONArray([JSONValue])
    case JSONBool(Bool)
    case JSONNull
}

相似地,若是咱們解析了其它的東西,也可使用這種方式將解析結果轉化咱們 Swift 的類型。

UIKit 標識

枚舉能夠用來將字符串類型的重用標識或者 storyboard 標識映射爲類型系統能夠進行檢查的類型。假設咱們有一個擁有不少原型 Cell 的 UITableView:

enum CellType: String {
    case ButtonValueCell = "ButtonValueCell"
    case UnitEditCell = "UnitEditCell"
    case LabelCell = "LabelCell"
    case ResultLabelCell = "ResultLabelCell"
}

單位

單位以及單位轉換是另外一個使用枚舉的絕佳場合。能夠將單位及其對應的轉換率映射起來,而後添加方法來對單位進行自動的轉換。如下是一個至關簡單的示例:

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))

另外一個示例是貨幣的轉換。以及數學符號(好比角度與弧度)也能夠從中受益。

遊戲

遊戲也是枚舉中的另外一個至關好的用例,屏幕上的大多數實體都屬於一個特定種族的類型(敵人,障礙,紋理,...)。相對於本地的 iOS 或者 Mac 應用,遊戲更像是一個白板。即開發遊戲咱們可使用全新的對象以及全新的關聯創造一個全新的世界,而 iOS 或者 OSX 須要使用預約義的 UIButtons,UITableViews,UITableViewCells 或者 NSStackView.

不只如此,因爲枚舉能夠遵照協議,咱們能夠利用協議擴展和基於協議的編程爲不一樣爲遊戲定義的枚舉增長功能。這裏是一個用來展現這種層級的的簡短示例:

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 {}

字符串類型化

在一個稍微大一點的 Xcode 項目中,咱們很快就會有一大堆經過字符串來訪問的資源。在前面的小節中,咱們已經提太重用標識和 storyboard 的標識,可是除了這兩樣,還存在不少資源:圖像,Segues,Nibs,字體以及其它資源。一般狀況下,這些資源均可以分紅不一樣的集合。若是是這樣的話,一個類型化的字符串會是一個讓編譯器幫咱們進行類型檢查的好方法。

enum DetailViewImages: String {
  case Background = "bg1.png"
  case Sidebar = "sbg.png"
  case ActionButton1 = "btn1_1.png"
  case ActionButton2 = "btn2_1.png"
}

對於 iOS 開發者,R.swift這個第三方庫能夠爲以上提到的狀況自動生成結構體。可是有些時候你可能須要有更多的控制(或者你多是一個Mac開發者8)。

API 端點

Rest API 是枚舉的絕佳用例。它們都是分組的,它們都是有限的 API 集合,而且它們也可能會有附加的查詢或者命名的參數,而這可使用關聯值來實現。

這裏有個 Instagram API 的簡化版:

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框架就是基本這個思想,使用枚舉對 rest 端點進行映射。

鏈表

Airspeed Velocity有一篇極好的文章說明了如何使用枚舉來實現一個鏈表。那篇文章中的大多數代碼都超出了枚舉的知識,並涉及到了大量其它有趣的主題9,可是,鏈表最基本的定義是相似這樣的(我對其進行了一些簡化):

enum List {
    case End
    indirect case Node(Int, next: List)
}

每個節點(Node) case 都指向了下一個 case, 經過使用枚舉而非其它類型,咱們能夠避免使用一個可選的 next 類型以用來表示鏈表的結束。

Airspeed Velocity 還寫過一篇超讚的博客,關於如何使用 Swift 的間接枚舉類型來實現紅黑樹,因此若是你已經閱讀過關於鏈表的博客,你可能想繼續閱讀這篇關於紅黑樹的博客

設置字典(Setting Dictionaries)

這是 Erica Sadun 提出的很是很是機智的解決方案。簡單來說,就是任何咱們須要用一個屬性的字典來對一個項進行設置的時候,都應該使用一系列有關聯值的枚舉來替代。使用這方法,類型檢查系統能夠確保配置的值都是正確的類型。

關於更多的細節,以及合適的例子,能夠閱讀下她的文章

侷限

與以前相似,我將會用一系列枚舉的侷限性來結束本篇文章。

提取關聯值

David Owens寫過一篇文章,他以爲當前的關聯值提取方式是很笨重的。我牆裂推薦你去看一下他的原文,在這裏我對它的要旨進行下說明:爲了從一個枚舉中獲取關聯值,咱們必須使用模式匹配。然而,關聯值就是關聯在特定枚舉 case 的高效元組。而元組是可使用更簡單的方式來獲取它內部值,即 .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)

若是你也一樣以爲咱們應該使用相同的方法來對枚舉進行解構(deconstruct),這裏有個 rdar: rdar://22704262 (譯者注:一開始我不明白 rdar 是啥意思,後來我 google 了下,若是你也有興趣,也能夠本身去搜索一下)

相等性

擁有關聯值的枚舉沒有遵照 equatable 協議。這是一個遺憾,由於它爲不少事情增長了沒必要要的複雜和麻煩。深層的緣由多是由於關聯值的底層使用是使用了元組,而元組並無遵照 equatable 協議。然而,對於限定的 case 子集,若是這些關聯值的類型都遵照了 equatable 類型,我認爲編譯器應該默認爲其生成 equatable 擴展。

// Int 和 String 是可判等的, 因此 Mode 應該也是可判等的
enum Ex { case Mode(ab: Int, cd: String) }

// Swift 應該可以自動生成這個函數
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)

最大的問題就是對元組的支持。我喜歡使用元組,它們可使不少事情變得更簡單,可是他們目前還處於無文檔狀態而且在不少場合都沒法使用。在枚舉當中,咱們沒法使用元組做爲枚舉的值:

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

這彷佛看起來並非一個最好的示例,可是咱們一旦開始使用枚舉,就會常常陷入到須要用到相似上面這個示例的情形中。

迭代枚舉的全部case

這個咱們已經在前面討論過了。目前尚未一個很好的方法來得到枚舉中的全部 case 的集合以使咱們能夠對其進行迭代。

默認關聯值

另外一個會碰到的事是枚舉的關聯值老是類型,可是咱們卻沒法爲這些類型指定默認值。假設有這樣一種狀況:

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)
}

咱們依然可使用不一樣的值建立新的 case,可是角色的默認設置依然會被映射。

變化

10/26/2015

  • 增長侷限性示例(相等性 & 獲取關聯值)

  • 增長 Erica Sadun 的關聯枚舉示例

10/22/2015

  • 合併來自 #6 @mabidakun的PR

  • 增長枚舉底層的連接

  • 將賬號示例拆分爲兩個更容易理解的片斷。

10/21/2015

解釋

  • 一、可使用一些小技術來達到這個目的,具體的請參照下面的文章內容

  • 二、爲了演示的緣故,這個示例的實現通過的簡化。在真實的開發中,應該使用可選類型以及反向順序的參數。能夠參考一下如今十分流行的函數式編程庫,如 SwiftzDollar

  • 三、這個示例直接採用了Swift 官方文檔的示例

  • 四、常常使得他們定義的位置很難被發現

  • 五、這是一個簡化版的,固然,Swift 爲咱們加了不少的語法糖

  • 六、若是你在應用中使用過 JSON,應該也曾經碰到過這個問題

  • 七、順便一提,不能直接使用數字作爲枚舉 case 的名稱,所以直接使用 400 是不行的

  • 八、雖然如此,不過支持 Mac 版的 R.swift 好像就快推出了

  • 九、這句話能夠解釋爲: 打開連接,並開始閱讀文章

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索