可以匹配就匹配咯: 詳解 Swift 的模式匹配

怒戳查看最終稿@SwiftGG

在衆多 Swift 提供給 Objective-C 程序員使用的新特性中,有個特性把自己僞裝成一個無聊的老頭,但是卻在如何優雅滴解決“鞭屍金字塔“的問題上有着巨大的潛力。很顯然我所說的這個特性就是 switch 語句, 對於很多 Objective-C 程序員來說,除了用在 Duff’s Device 上比較有趣之外,switch 語句非常笨拙,與多個 if 語句相比,它幾乎沒有任何優勢。

不過 Swift 中的 switch 語句能做的就多了。在接下來的教程裏,我會更加詳細滴講解這些新特性的各種用途。我會忽略那些與Objective-C 和 C 中 switch 語句相比沒有任何優勢的解決方案。這篇文章基礎的部分寫於 2014 年 7 月,但是很多我寫的模式都會導致編譯器崩潰,所以我只好推遲這些內容的編寫,直到編譯器能提供更好的支持。

這篇博客還有如下語言的版本:

日語 (感謝 M Ono!)

開始咯

switch 語句主要的特性當然是模式匹配咯,模式匹配可以對值進行解構,然後根據相應 case 的正確匹配值來進行匹配。

// 歷史上最壞的一個例子:二進制->十進制的轉換
let bool1 = 1
let bool2 = 0
switch (bool1, bool2) {
   case (0, 0): print("0")
   case (0, 1): print("1")
   case (1, 0): print("2")
   case (1, 1): print("3")
}

模式匹配很早以前就在其他語言中存在了,這些語言包括 Haskell、Erlang、Scala和Prolog。這是一個福音,因爲這允許我們觀察那些語言是如何利用模式匹配來解決它們的問題的。我們甚至可以通過觀察它們的例子來找到最實用的那個。

一個交易引擎

於是華爾街聯繫你了,他們需要一個新的運行在 iOS 設備上的交易平臺。因爲是交易平臺,所以你需要給交易定義一個 enum

第一步

enum Trades {
    case Buy(stock: String, amount: Int, stockPrice: Float)
    case Sell(stock: String, amount: Int, stockPrice: Float)
}

同時還會提供如下的 API 給你來進行交易處理。注意銷售訂單的金額是如何變成負數的,而且你還會被告知股票的價格是不重要的,他們的引擎會在內部選擇一個。

/**
 - 參數 stock: 股票的名字
 - 參數 amount: 金額, 負數表示銷售額, 正數表示購買額
*/
func process(stock: String, _ amount: Int) {
    print ("\(amount) of \(stock)")
}

下一步就是對交易進行處理。你會發現模式匹配在寫這個業務時表現出的強大處理能力:

let aTrade = Trades.Buy(stock: "APPL", amount: 200, stockPrice: 115.5)

switch aTrade {
case .Buy(let stock, let amount, _):
    process(stock, amount)
case .Sell(let stock, let amount, _):
    process(stock, amount * -1)
}
// 輸出 "buy 200 of APPL"

Swift 可以讓我們非常方便滴從 enum 中解構/提取出我們真正想要的信息。在這個例子中只有 stockamount 被解構出來。

真棒,現在你可以去華爾街展示這個極好的交易平臺了。然而,現實往往比美好的想象要殘酷得多。你以爲交易就是你以爲的交易麼?

  • 你必須根據不同的交易方式計算費用。
  • 機構越小,費用越高
  • 而且,機構越大,優先級越高。

華爾街的人也意識到要處理這些問題你需要新的 API,所以他們給了你下面的兩個:

func processSlow(stock: String, _ amount: Int, _ fee: Float) { print("slow") }
func processFast(stock: String, _ amount: Int, _ fee: Float) { print("fast") }

交易類型

於是你回到繪圖板重新增加了一個 enum。交易類型也是每個交易的一部分。

enum TraderType {
case SingleGuy
case Company
} 

enum Trades {
    case Buy(stock: String, amount: Int, stockPrice: Float, type: TraderType)
    case Sell(stock: String, amount: Int, stockPrice: Float, type: TraderType)
}

所以,如何去最好滴實現這一新的機制呢?你可以用一個 if / else 分支來實現購買和銷售,但是這會導致代碼嵌套以至於很快代碼就變的不清晰了——而且誰知道那些華爾街人會不會給你找新的麻煩。所以你應該把它定義爲模式匹配的一個新要求:

let aTrade = Trades.Sell(stock: "GOOG", amount: 100, stockPrice: 666.0, type: TraderType.Company)

switch aTrade {
case let .Buy(stock, amount, _, TraderType.SingleGuy):
    processSlow(stock, amount, 5.0)
case let .Sell(stock, amount, _, TraderType.SingleGuy):
    processSlow(stock, -1 * amount, 5.0)
case let .Buy(stock, amount, _, TraderType.Company):
    processFast(stock, amount, 2.0)
case let .Sell(stock, amount, _, TraderType.Company):
    processFast(stock, -1 * amount, 2.0)
}

這段代碼的優雅之處在於它非常簡潔的描述了不同可能的組合。注意我們是如何把 .Buy(let stock, let amount) 修改成 let .Buy(stock, amount) 來讓事情更簡單一些。這樣就可以用更少的語句來像之前一樣對 enum 進行解構。

警衛!警衛!呼叫警衛!

於是你再次向你的華爾街用戶展示你的開發成果,而他們則又提出了新的問題(你真應該把項目的細節問得更清楚一點)。

  • 交易總額超過 1.000.000$ 的銷售訂單通常需要更快進行處理,就算是個人客戶也得這樣。
  • 交易總額小於 1.000$ 的購買訂單通常處理更慢。

如果使用傳統的 if 語句,這時代碼就應該已經有點凌亂了,而 switch 就不會。Swift 爲 switch cases 提供了保護機制,這種機制可以讓你進一步滴對可能匹配的 case 進行約束。

你只需要對 switch 語句稍作修改就可以滿足新的變化。

let aTrade = Trades.Buy(stock: "GOOG", amount: 1000, stockPrice: 666.0, type: TraderType.SingleGuy)

switch aTrade {
case let .Buy(stock, amount, _, TraderType.SingleGuy):
    processSlow(stock, amount, 5.0)
case let .Sell(stock, amount, price, TraderType.SingleGuy)
    where price*Float(amount) > 1000000:
    processFast(stock, -1 * amount, 5.0)
case let .Sell(stock, amount, _, TraderType.SingleGuy):
    processSlow(stock, -1 * amount, 5.0)
case let .Buy(stock, amount, price, TraderType.Company)
    where price*Float(amount) < 1000:
    processSlow(stock, amount, 2.0)
case let .Buy(stock, amount, _, TraderType.Company):
    processFast(stock, amount, 2.0)
case let .Sell(stock, amount, _, TraderType.Company):
    processFast(stock, -1 * amount, 2.0)
}

上面的代碼結構很清晰,閱讀起來也相當簡單,對複雜情況的封裝也很好。

就是這樣,我們已經成功滴實現了我們的交易引擎。然而,這個解決方案還是有點繁瑣;我們在想是否還有對其進行改進的模式匹配方案。所以,讓我們繼續深入研究一下模式匹配。

模式匹配進階

現在我們在實戰中已經見過了幾種模式。但其語法是什麼?還能匹配什麼?Swift 將這些模式分爲 7 種。我們現在就來認識一下它們。

所有的這些模式不僅能用在 switch 關鍵詞上,而且可以用在 ifguardfor 關鍵詞上。如需瞭解詳情,接着看下面的內容。

1. 通配符模式

通配符模式會忽略需要匹配的值,這種 case 下任何值都是有可能的。這和 let _ = fn() 一樣的模式,在這個模式下, _ 表示你將不再使用這個值。有意思的是這個模式可以匹配包括 nil 在內的所有值 1 。如果增加一個 ?,它甚至可以匹配可選值:

let p: String? = nil
switch p {
case _?: print ("Has String")
case nil: print ("No String")
}

就像你在交易例子裏面看到的一樣,它也允許你忽略需要匹配的 enum 或者 tuples 中無用的數據:

switch (15, "example", 3.14) {
    case (_, _, let pi): print ("pi: \(pi)")
}

2. 標示模式

匹配一個具體的值。這個和 Objective-C 的 switch 實現是一樣的:

switch 5 {
  case 5: print("5")
}

3. 值綁定模式

這種模式和通過 let 或者 var 綁定值到變量中一樣。而且僅僅只在一個 switch 語句中實現。因爲你之前已經見到過,所以我只給出一個非常簡單的例子:

switch (4, 5) {
  case let (x, y): print("\(x) \(y)")
}

4. 元組模式

關於元組我已經寫了一整篇博文,這篇博文所提供的信息遠遠比這裏多,但是我還是在這裏給出一個簡短的例子:

let age = 23
let job: String? = "Operator"
let payload: AnyObject = NSDictionary()

switch (age, job, payload) {
  case (let age, _?, _ as NSDictionary):
  print(age)
  default: ()
}

在這裏,我們把 3 個值結合放到一個元組中(假想它們是通過調用不同的 API 得到的),然後一口氣匹配它們,注意這個模式完成了三件事情:

  1. 提取 age
  2. 確保存在一個 job,就算我們不需要它
  3. 確保 payload 的類型是 NSDictionary,儘管我們同樣不需要訪問它的具體值。

5. 枚舉 Case 模式(Enumeration Case Pattern)

就如你在交易例子中所見,模式匹配對 Swift 的 enum 支持得相當棒。這是因爲 enum cases 就像密封、不可變且可解構的結構體。這非常像 tuples,你可以打開正好匹配上的某個單獨 case 的內容然後只抽取出你需要的信息2

假想你正在用函數式的風格寫一個遊戲,然後你需要定義一些實體。你可以使用 structs 但是你的實體的狀態很少,你覺得這樣有點矯枉過正。

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

現在你需要實現繪圖循環。這裏我們只需要 X 和 Y 座標:

for e in entities() {
    switch e {
    case let .Soldier(x, y):
      drawImage("soldier.png", x, y)
    case let .Tank(x, y):
      drawImage("tank.png", x, y)
    case let .Player(x, y):
      drawImage("player.png", x, y)
    }
}

6. 類型轉換模式

就像名字所表示的一樣,這種模式轉換或者匹配類型。它有兩種不同的關鍵詞:

  • is 類型:匹配右手邊內容的運行時類型(或者類型的子類)。它會做類型轉換但是不關注返回值。所以你的 case 塊不知道所匹配的類型是什麼。
  • 模式 as 類型:和 is 模式做同樣的匹配操作,但是如果成功的話會把類型轉換到左側指定的模式中。

下面是這兩種關鍵詞的例子:

let a: Any = 5 
switch a {
  // 這會失敗因爲它的類型仍然是 `Any`
  // 錯誤: binary operator '+' cannot be applied to operands of type 'Any' and 'Int'
  case is Int: print (a + 1)
  // 有效並返回 '6'
  case let n as Int: print (n + 1)
  default: ()
}

注意 is 前沒有 pattern。它直接和 a 做匹配。

7. 表達模式

表達模式非常強大。它可以把 switch 的值和實現了 ~= 操作符的表達式進行匹配。而且對於這個操作符有默認的實現,比如對於範圍匹配,你可以這樣做:

switch 5 {
 case 0..10: print("In range 0-10")
}

然而,更有趣的可能是自己重寫操作符,然後使你的自定義類型可以匹配。我們假定你想重寫之前寫的士兵遊戲,而且你無論如何都要使用結構體。

struct Soldier {
  let hp: Int
  let x: Int
  let y: Int
}

現在你想輕鬆滴匹配所有血量爲 0 的實體。我們可以像下面一樣實現 ~= 操作符。

func ~= (pattern: Int, value: Soldier) -> Bool {
    return pattern == value.hp
}

現在我們就可以對一個實體做匹配了:

let soldier = Soldier(hp: 99, x: 10, y: 10)
switch soldier {
   case 0: print("dead soldier")
   default: ()
}

不幸滴是,對元組做全匹配似乎不好使。如果你實現下面的代碼,就會出現類型檢查錯誤。

func ~= (pattern: (hp: Int, x: Int, y: Int), value: Soldier) -> Bool {
   let (hp, x, y) = pattern
   return hp == value.hp && x == value.x && y == value.y
}

一個可能解決上述類似問題的方案是給你的 struct 增加一個 unapply 方法然後再進行匹配:

extension Soldier {
   func unapply() -> (Int, Int, Int) {
      return (self.hp, self.x, self.y)
   }
}

func ~= (p: (Int, Int, Int), t: (Int, Int, Int)) -> Bool {
   return p.0 == t.0 && p.1 == t.1 && p.2 == t.2 
}

let soldier = Soldier(hp: 99, x: 10, y: 10)
print(soldier.unapply() ~= (99, 10, 10))

但是這相當麻煩而且沒有利用好模式匹配背後的大量魔法。

在這篇博文之前的版本中我寫過 ~= 不適用於協議,但是我錯了。我記得我在一個 Playground 中試過。而這個例子(由 reddit 上的 latrodectus 友情提供)是完全可用的:

protocol Entity {
    var value: Int {get}
}

struct Tank: Entity {
    var value: Int
    init(_ value: Int) { self.value = value }
}

struct Peasant: Entity {
    var value: Int
    init(_ value: Int) { self.value = value }
}

func ~=(pattern: Entity, x: Entity) -> Bool {
    return pattern.value == x.value
}

switch Tank(42) {
    case Peasant(42): print("Matched") // 匹配成功
    default: ()
}

你可以利用 Expression Patterns 做很多事情。如果想要了解更多表達模式的細節,看看這篇由 Austin Zheng 寫的超棒博文

現在我們已經講完了所有可能的 switch 模式。在我們繼續講解之前,還需要討論最後一件事情。

fallthrough,break 和標籤

下面的內容和模式匹配沒有直接關係,僅僅是和 switch 關鍵詞有關,所以我就簡單說了。默認來說,和 C/C++/Objective-C不一樣的是:switch cases 不會自動進入下一個 case,這也是爲什麼 Swift 不需要給每個 case 都寫上 break。你可以選擇使用 fallthrough 關鍵詞來實現傳統的自動進入下一個 case 的行爲。

switch 5 {
   case 5:
    print("Is 5")
    fallthrough
   default:
    print("Is a number")
}
// 會在命令行輸出: "Is 5" "Is a number"

另外,你可以使用 break 來提前跳出 switch 語句。既然不會默認進入下一個 case,爲什麼還需要這麼做呢?比如你知道在一個 case 中有一個必須的要求是不滿足的,這樣你就不能繼續執行這個 case 了:

let userType = "system"
let userID = 10
switch (userType, userID)  {
   case ("system", _):
     guard let userData = getSystemUser(userID) else { break }
     print("user info: \(userData)")
     insertIntoRemoteDB(userData)
   default: ()
}
... 更多你需要執行的代碼

在這段代碼中,當 getSystemUser 返回的結果是 nil 時你不想再繼續調用 insertIntoRemoteData。當然,你可以在這裏使用 if let,但是如果多個這樣的情況結合到一起的時候,很快你就會得到一堆可怕醜陋的 if lets 嵌套代碼。

但是如果你是在一個 while 循環中執行你的 switch 語句,然後你想跳出循環,而不是 switch 的時候,你需要怎麼做呢?對與這種情況, Swift 允許你定義一個 labels ,然後 break 或者 continue 到這個 labels

gameLoop: while true {
  switch state() {
     case .Waiting: continue gameLoop
     case .Done: calculateNextState()
     case .GameOver: break gameLoop
  }
}

我們已經討論過 switch 和模式匹配的語法和實現細節。現在,讓我們來看一些有趣(多少有點)的真實案例。

真實案例

可選值

對可選值進行解包的方式有很多種,模式匹配就是其中一種。可能到現在這種方法你已經用得非常頻繁了,但還是給一個簡短的例子吧:

var result: String? = secretMethod()
switch result {
case .None:
    println("is nothing")
case let a:
    println("\(a) is a value")
}

如果是 Swift 2.0 的話,這會更簡單:

var result: String? = secretMethod()
switch result {
case nil:
    print("is nothing")
case let a?:
    print("\(a) is a value")
}

正如你所見,result 可以是一個字符串,但是它也可能是 nil,因爲它是 optional 值。通過對 result 執行 switch。我們可以確定它是 .None 或者是一個確定的值。更進一步,如果他是一個確定的值,我們可以在 a 這種情況下馬上把這個值綁定到一個變量。這段代碼代碼的優美之處在於:變量 result 可能存在的兩種狀態被非常明顯滴區分開來。

類型匹配

做爲強類型體系,Swift 通常不會像 Objective-C 那樣經常需要運行時類型檢查。然而,當你需要與傳統的 Objective-C 代碼交互時(這還沒有更新到簡單泛型的反射一文中),那你就經常會碰到需要做類型檢查的代碼。假想你得到了一個包含 NSStringNSNumber 元素的數組:

let u = NSArray(array: [NSString(string: "String1"), NSNumber(int: 20), NSNumber(int: 40)])

當你遍歷這個 NSArray 時,你永遠不知道你得到的是什麼類型。然而, switch 語句可以讓你很簡單滴在這裏測試這些類型:

for x in u {
    switch x {
    case _ as NSString:
    print("string")
    case _ as NSNumber:
    print("number")
    default:
    print("Unknown types")
    }
}

按範圍做分級

現在你正在給你當地的高校寫分級的 iOS 應用。老師想要輸入一個 0 到 100 的數值,然後得到一個相應的等級字符(A-F)。模式匹配現在要來拯救你了:

let aGrade = 84

switch aGrade {
case 90...100: print("A")
case 80...90: print("B")
case 70...80: print("C")
case 60...70: print("D")
case 0...60: print("F")
default:
    print("Incorrect Grade")
}

字頻率統計

有一系列的數據對,每個數據對代表一個字和它在某段文字中出現的頻率。我們的目標就是把那些低於或者高於某個固定閾值的數據對過濾掉,然後只返回剩下的不包含其頻率的所有字。

這是我們的字集:

let wordFreqs = [("k", 5), ("a", 7), ("b", 3)]

一個簡單的解決方案是使用 mapfilter 進行建模:

let res = wordFreqs.filter({ (e) -> Bool in
    if e.1 > 3 {
    return true
    } else {
    return false
    }
}).map { $0.0 }
print(res)

然而,因爲 flatmap 只能返回非空元素,所以這個解決方案還有很大的改進空間。首先最重要的是,我們可以放棄使用 e.1 而利用元組來做適當的解構(你猜對了)。然後我們只需要調用一次 flatmap,就可以減少先 filter 然後 map 所帶來的不必要的性能開銷。

let res = wordFreqs.flatMap { (e) -> String? in
    switch e {
    case let (s, t) where t > 3: return s
    default: return nil
    }
}
print(res)

遍歷目錄

假想你需要遍歷一個文件樹然後查找以下內容:

  • 所有 customer1 和 customer2 創建的 “psd“文件
  • 所有 customer2 創建的 “blend“文件
  • 所有用戶創建的 “jpeg“文件
guard let enumerator = NSFileManager.defaultManager().enumeratorAtPath("/customers/2014/")
else { return }

for url in enumerator {
    switch (url.pathComponents, url.pathExtension) {

    // customer1 和 customer2 創建的 “psd“文件
    case (let f, "psd") 
        where f.contains("customer1") 
        || f.contains("customer2"): print(url)

    // customer2 創建的 “blend“文件
    case (let f, "blend") where f.contains("customer2"): print(url)

    // 所有的 “jpeg“文件
    case (_, "jpg"): print(url)

    default: ()
    }
}

注意 contains 在第一個匹配就結束然後就不用遍歷完整的路徑了。同樣,模式匹配的代碼非常簡潔明瞭。

Fibonacci

同樣,來看一下使用模式匹配實現的 fibonacci 算法有多優美3

func fibonacci(i: Int) -> Int {
    switch(i) {
    case let n where n <= 0: return 0
    case 0, 1: return 1
    case let n: return fibonacci(n - 1) + fibonacci(n - 2)
    }
}

print(fibonacci(8))

當然,如果是大數的話,程序棧會爆掉。

傳統的 API 和值提取

通常情況下,當你從外部源取數據的時候,比如一個庫,或者一個 API,它不僅是一種很好的做法,而且通常在解析數據之前需要檢查數據的一致性。你需要確保所有的 key 都是存在的、或者數據的類型都正確、或者數組的長度滿足要求。如果不這麼做就會因爲bug(有的 key 沒有)而導致 app 崩潰(索引不存在的數組項)。而傳統的做法通常是嵌套 if 語句。

假想有 API 返回一條用戶信息。但是有兩種類型的用戶:系統用戶——如管理員或者郵政局長——和本地用戶——如 “John B“、“Bill Gates“等。因爲系統的設計和增長,API 的使用者需要處理一些麻煩的事情:

  • systemlocal 用戶來自同一個 API 調用。
  • 因爲早期版本的數據庫沒有 department 這個字段,所以這個 key 可能是不存在的,而且早期的僱員從來都不需要填寫這個字段。
  • 根據用戶被創建的時間,name 數組可能包含 4 個元素(username,middlename,lastname 和 firstname)或者 2 個元素(full name,username)
  • age 是代表用戶年齡的整型數

我們的系統需要給這個 API 返回的所有系統用戶創建用戶賬號,賬號信息只包含如下信息:username 和 department。我們只需要 1980 年以前出生的用戶。如果沒有指定 department,就指定爲 “Corp“。

func legacyAPI(id: Int) -> [String: AnyObject] {
    return ["type": "system", "department": "Dark Arts", "age": 57, 
       "name": ["voldemort", "Tom", "Marvolo", "Riddle"]] 
}

我們爲給定的約束實現一個模式來進行匹配:

let item = legacyAPI(4)
switch (item["type"], item["department"], item["age"], item["name"]) {
   case let (sys as String, dep as String, age as Int, name as [String]) where 
      age < 1980 &&
      sys == "system":
     createSystemUser(name.count == 2 ? name.last! : name.first!, dep: dep ?? "Corp")
  default:()
}

// 返回 ("voldemort", "Dark Arts")

注意這段代碼做了一個很危險的假設:就是如果 name 數組元素的個數不是 2 個的話,那麼它一定包含 4 個元素。如果這種假設不成立,我們獲得了包含 0 個元素的數組,這段代碼就會崩潰。

除了這一點,模式匹配向你展示了它是如何在只有一個 case 的情況下幫助你編寫乾淨的代碼和簡化值的提取的。

同樣來看看我們是怎麼寫緊跟在 case 之後 let 的,這樣一來就不必在每一次賦值的時候都重複寫它。

模式和其他關鍵詞

Swift 的文檔指出不是所有的模式都可以在 iffor 或者 guard 語句中使用。然而,這個文檔似乎不是最新的。所有 7 種模式對這三個關鍵詞都有效。

我爲那些感興趣的人編了一個例子要點,爲每個模式和每個關鍵詞都寫了一個例子。

你可以在這裏查看所有的樣例模式

來看一個對三個關鍵詞使用 值綁定元組類型轉換模式的簡短例子:

// 到嗎編譯後只是一個關鍵詞的集合。其本身沒有任何意義
func valueTupleType(a: (Int, Any)) -> Bool {
    // guard case 的例子
    guard case let (x, _ as String) = a else { return false}
    print(x)

    // for case 的例子
    for case let (a, _ as String) in [a] {
    print(a)
    }

    // if case 的例子
    if case let (x, _ as String) = a {
       print("if", x)
    }

    // switch case example
    switch a {
    case let (a, _ as String):
    print(a)
    return true
    default: return false
    }
}
let u: Any = "a"
let b: Any = 5
print(valueTupleType((5, u)))
print(valueTupleType((5, b)))
// 5, 5, "if 5", 5, true, false

我們可以帶着這個想法詳細地看一看每一個關鍵詞。

使用 for case

到了 Swift 2.0 後,模式匹配變得更加重要,因爲它被擴展到不僅可以支持 switch ,還可以支持其他的關鍵詞。比如,讓我們寫一個簡單的只返回非空元素的數組函數:

func nonnil<T>(array: [T?]) -> [T] {
   var result: [T] = []
   for case let x? in array {
      result.append(x)
   }
   return result
}

print(nonnil(["a", nil, "b", "c", nil]))

關鍵詞 case 可以被 for 循環使用,就像 switch 中的 case 一樣。下面是另外一個例子。還記得我們之前說的遊戲麼?經過第一次重構之後,現在我們的實體系統看起來是這樣的:

enum Entity {
    enum EntityType {
    case Soldier
    case Player
    }
    case Entry(type: EntityType, x: Int, y: Int, hp: Int)
}

真棒!這可以讓我們用更少的代碼繪製出所有的項目:

for case let Entity.Entry(t, x, y, _) in gameEntities()
where x > 0 && y > 0 {
    drawEntity(t, x, y)
}

我們用一行就解析出了所有必需的屬性,然後確保我們不會在 0 一下的範圍繪製,最後我們調用渲染方法(drawEntity)。

爲了知道選手是否在遊戲中勝出,我們想要知道是否有至少一個士兵的血量是大於 0 的。

func gameOver() -> Bool {
    for case Entity.Entry(.Soldier, _, _, let hp) in gameEntities() 
    where hp > 0 {return false}
    return true
}
print(gameOver())

好的是 Soldier 的匹配是 for 查詢的一部分。這感覺有點像 SQL 而不是命令循環編程。同時,這也可以讓編譯器更清晰滴知道我們的意圖,從而就有了打通調度增強這條路的可能性。另外一個很好的體驗就是我們不需要完成滴拼寫出 Entity.EntityType.Soldier。就算我們像上面一樣只寫 .Soldier,Swift 也能明白我們的意圖。

使用 guard case

另外一個支持模式匹配的關鍵詞就是新引入的 guard 關鍵詞。它允許你像 if let 一樣把 optionals 綁定到本地範圍,而且不需要任何嵌套:

func example(a: String?) {
    guard let a = a else { return }
    print(a)
}
example("yes")

guard let case 允許你做一些類似模式匹配所介紹的事情。讓我們再來看一下士兵的例子。在玩家的血量變滿之前,我們需要計算需要增加的血量。士兵不能漲血,所以對於士兵實體而言,我們始終返回 0。

let MAX_HP = 100

func healthHP(entity: Entity) -> Int {
    guard case let Entity.Entry(.Player, _, _, hp) = entity 
    where hp < MAX_HP 
    else { return 0 }
    return MAX_HP - hp
}

print("Soldier", healthHP(Entity.Entry(type: .Soldier, x: 10, y: 10, hp: 79)))
print("Player", healthHP(Entity.Entry(type: .Player, x: 10, y: 10, hp: 57)))

// 輸出:
"Soldier 0"
"Player 43"

這是把我們目前討論的各種機制用到極致的一個例子。

  • 它非常清晰,沒有牽扯到任何嵌套
  • 狀態的邏輯和初始化是在 func 之前處理的,這樣可以提高代碼的可讀性
  • 非常簡潔

這也是 switchfor 的完美結合,可以把複雜的邏輯結構封裝成易讀的格式。當然,它不會讓邏輯變得更容易理解,但是至少會以更清晰的方式展現給你。特別是使用 enums 的時候。

使用 if case

if case 的作用和 guard case 相反。它是一種非常棒滴在分支中打開和匹配數據的方式。結合之前 guard 的例子。很顯然,我們需要一個 move 函數,這個函數允許我們說一個實體在朝一個方向移動。因爲我們的實體是 enums,所以我們需要返回一個更新過的實體。

func move(entity: Entity, xd: Int, yd: Int) -> Entity {
    if case Entity.Entry(let t, let x, let y, let hp) = entity
    where (x + xd) < 1000 &&
        (y + yd) < 1000 {
    return Entity.Entry(type: t, x: (x + xd), y: (y + yd), hp: hp)
    }
    return entity
}
print(move(Entity.Entry(type: .Soldier, x: 10, y: 10, hp: 79), xd: 30, yd: 500))
// 輸出: Entry(main.Entity.EntityType.Soldier, 40, 510, 79)

限制

一些限制已經在文章中說過,比如有關 Expression Patterns 的問題,看起來它似乎不能匹配 tuples (那樣的話就真的很方便了)。在 Scala 和 Clojure 中,模式匹配在集合上同樣可用,所以你可以匹配它的頭部、尾部和部分等。4。這在 Swift 中是不支持的(儘管 Austin Zheng 在我之前鏈接的博客裏差不多實現了這一點

另外一種不可用的的情況是(這一點 Scala 同樣做得很好)對類或者結構體進行解構。Swift 允許我們定義一個 unapply 方法,這個方法做的事情大體和 init 相反。實現這個方法,然後就可以讓類型檢查器對類進行匹配。而在 Swift 中,它看起來就像下面一樣:

struct Imaginary {
   let x: Int
   let y: Int
   func unapply() -> (Int, Int) {
     // 實現這個方法之後,理論上來說實現瞭解構變量所需的所有細節
     return (self.x, self.y)
   }
}
// 然後這個就會自動 unapply 然後再進行匹配
guard case let Imaginary(x, y) = anImaginaryObject else { break }

更新

08/21/2015 結合 Reddit 上 foBrowsing 的有用反饋

  • 增加 guard case let
  • 增加簡化版的 let 語法(如:let (x, y) 替代 (let x, let y)

08/22/2015 似乎有一些東西我沒測試好。我列舉的一些限制實際上是可用的,另外一個 Reddit 上的評論者(latrodectus)提出了一些非常有用的指正。

  • 將之前的修正爲:所有的模式對三個關鍵詞都適用,然後增加了一些要點案例
  • 關於協議和表達式模式無效這個限制,其實沒有的
  • 增加 “模式可用性“章節

08/24/2015

  • 增加 if case 樣例,重命名了一些章節。
  • 修復了一些文本拼寫錯誤。尤其我不小心寫道:_ 不能匹配 nil。那當然是不對的,_ 可以匹配所有的東西。(感謝 obecker

09/18/2015

  • 添加了日語翻譯的鏈接

1.可以把它當做 shell 裏面的 * 通配符

2.我不清楚編譯器是否在對這點進行了優化,但理論上來說,它應該能計算出所需數據的正確位置,然後忽略 enum 的其他情況並內聯這個地址

3.當然,不是 Haskell實現的對手:
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

4.比如:switch [1, 2, 4, 3] {
case [, 2, , 3]:
}

發佈了61 篇原創文章 · 獲贊 167 · 訪問量 37萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章