在眾多 Swift 提供給 Objective-C 程序員使用的新特性中,有個特性把自己偽裝成一個無聊的老頭,但是卻在如何優(yōu)雅滴解決“鞭尸金字塔“的問題上有著巨大的潛力。很顯然我所說的這個特性就是 switch
語句, 對于很多 Objective-C 程序員來說,除了用在 Duff's Device 上比較有趣之外,switch
語句非常笨拙,與多個 if
語句相比,它幾乎沒有任何優(yōu)勢。
不過 Swift 中的 switch
語句能做的就多了。在接下來的教程里,我會更加詳細滴講解這些新特性的各種用途。我會忽略那些與Objective-C 和 C 中 switch
語句相比沒有任何優(yōu)勢的解決方案。這篇文章基礎(chǔ)的部分寫于 2014 年 7 月,但是很多我寫的模式都會導(dǎo)致編譯器崩潰,所以我只好推遲這些內(nèi)容的編寫,直到編譯器能提供更好的支持。
這篇博客還有如下語言的版本:
switch
語句主要的特性當(dāng)然是模式匹配咯,模式匹配可以對值進行解構(gòu),然后根據(jù)相應(yīng) case
的正確匹配值來進行匹配。
// 歷史上最壞的一個例子:二進制->十進制的轉(zhuǎn)換
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。這是一個福音,因為這允許我們觀察那些語言是如何利用模式匹配來解決它們的問題的。我們甚至可以通過觀察它們的例子來找到最實用的那個。
于是華爾街聯(lián)系你了,他們需要一個新的運行在 iOS 設(shè)備上的交易平臺。因為是交易平臺,所以你需要給交易定義一個 enum
。
enum Trades {
case Buy(stock: String, amount: Int, stockPrice: Float)
case Sell(stock: String, amount: Int, stockPrice: Float)
}
同時還會提供如下的 API 給你來進行交易處理。注意銷售訂單的金額是如何變成負數(shù)的,而且你還會被告知股票的價格是不重要的,他們的引擎會在內(nèi)部選擇一個。
/**
- 參數(shù) stock: 股票的名字
- 參數(shù) amount: 金額, 負數(shù)表示銷售額, 正數(shù)表示購買額
*/
func process(stock: String, _ amount: Int) {
print ("\(amount) of \(stock)")
}
下一步就是對交易進行處理。你會發(fā)現(xiàn)模式匹配在寫這個業(yè)務(wù)時表現(xiàn)出的強大處理能力:
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
中解構(gòu)/提取出我們真正想要的信息。在這個例子中只有 stock
和 amount
被解構(gòu)出來。
真棒,現(xiàn)在你可以去華爾街展示這個極好的交易平臺了。然而,現(xiàn)實往往比美好的想象要殘酷得多。你以為交易就是你以為的交易么?
華爾街的人也意識到要處理這些問題你需要新的 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)
}
所以,如何去最好滴實現(xiàn)這一新的機制呢?你可以用一個 if / else
分支來實現(xiàn)購買和銷售,但是這會導(dǎo)致代碼嵌套以至于很快代碼就變的不清晰了——而且誰知道那些華爾街人會不會給你找新的麻煩。所以你應(yīng)該把它定義為模式匹配的一個新要求:
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)
}
這段代碼的優(yōu)雅之處在于它非常簡潔的描述了不同可能的組合。注意我們是如何把 .Buy(let stock, let amount)
修改成 let .Buy(stock, amount)
來讓事情更簡單一些。這樣就可以用更少的語句來像之前一樣對 enum
進行解構(gòu)。
于是你再次向你的華爾街用戶展示你的開發(fā)成果,而他們則又提出了新的問題(你真應(yīng)該把項目的細節(jié)問得更清楚一點)。
如果使用傳統(tǒng)的 if
語句,這時代碼就應(yīng)該已經(jīng)有點凌亂了,而 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)
}
上面的代碼結(jié)構(gòu)很清晰,閱讀起來也相當(dāng)簡單,對復(fù)雜情況的封裝也很好。
就是這樣,我們已經(jīng)成功滴實現(xiàn)了我們的交易引擎。然而,這個解決方案還是有點繁瑣;我們在想是否還有對其進行改進的模式匹配方案。所以,讓我們繼續(xù)深入研究一下模式匹配。
現(xiàn)在我們在實戰(zhàn)中已經(jīng)見過了幾種模式。但其語法是什么?還能匹配什么?Swift 將這些模式分為 7 種。我們現(xiàn)在就來認識一下它們。
所有的這些模式不僅能用在 switch
關(guān)鍵詞上,而且可以用在 if
,guard
和 for
關(guān)鍵詞上。如需了解詳情,接著看下面的內(nèi)容。
通配符模式會忽略需要匹配的值,這種 case
下任何值都是有可能的。這和 let _ = fn()
一樣的模式,在這個模式下, _
表示你將不再使用這個值。有意思的是這個模式可以匹配包括 nil
在內(nèi)的所有值 1 。如果增加一個 ?
,它甚至可以匹配可選值:
let p: String? = nil
switch p {
case _?: print ("Has String")
case nil: print ("No String")
}
就像你在交易例子里面看到的一樣,它也允許你忽略需要匹配的 enum
或者 tuples
中無用的數(shù)據(jù):
switch (15, "example", 3.14) {
case (_, _, let pi): print ("pi: \(pi)")
}
匹配一個具體的值。這個和 Objective-C 的 switch
實現(xiàn)是一樣的:
switch 5 {
case 5: print("5")
}
這種模式和通過 let
或者 var
綁定值到變量中一樣。而且僅僅只在一個 switch
語句中實現(xiàn)。因為你之前已經(jīng)見到過,所以我只給出一個非常簡單的例子:
switch (4, 5) {
case let (x, y): print("\(x) \(y)")
}
關(guān)于元組我已經(jīng)寫了一整篇博文,這篇博文所提供的信息遠遠比這里多,但是我還是在這里給出一個簡短的例子:
let age = 23
let job: String? = "Operator"
let payload: AnyObject = NSDictionary()
switch (age, job, payload) {
case (let age, _?, _ as NSDictionary):
print(age)
default: ()
}
在這里,我們把 3 個值結(jié)合放到一個元組中(假想它們是通過調(diào)用不同的 API 得到的),然后一口氣匹配它們,注意這個模式完成了三件事情:
age
job
,就算我們不需要它payload
的類型是 NSDictionary
,盡管我們同樣不需要訪問它的具體值。
就如你在交易例子中所見,模式匹配對 Swift 的 enum
支持得相當(dāng)棒。這是因為 enum cases
就像密封、不可變且可解構(gòu)的結(jié)構(gòu)體。這非常像 tuples
,你可以打開正好匹配上的某個單獨 case
的內(nèi)容然后只抽取出你需要的信息2。
假想你正在用函數(shù)式的風(fēng)格寫一個游戲,然后你需要定義一些實體。你可以使用 structs
但是你的實體的狀態(tài)很少,你覺得這樣有點矯枉過正。
enum Entities {
case Soldier(x: Int, y: Int)
case Tank(x: Int, y: Int)
case Player(x: Int, y: Int)
}
現(xiàn)在你需要實現(xiàn)繪圖循環(huán)。這里我們只需要 X 和 Y 坐標(biāo):
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)
}
}
就像名字所表示的一樣,這種模式轉(zhuǎn)換或者匹配類型。它有兩種不同的關(guān)鍵詞:
is
類型:匹配右手邊內(nèi)容的運行時類型(或者類型的子類)。它會做類型轉(zhuǎn)換但是不關(guān)注返回值。所以你的 case
塊不知道所匹配的類型是什么。as
類型:和 is
模式做同樣的匹配操作,但是如果成功的話會把類型轉(zhuǎn)換到左側(cè)指定的模式中。下面是這兩種關(guān)鍵詞的例子:
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
做匹配。
表達模式非常強大。它可以把 switch
的值和實現(xiàn)了 ~=
操作符的表達式進行匹配。而且對于這個操作符有默認的實現(xiàn),比如對于范圍匹配,你可以這樣做:
switch 5 {
case 0..10: print("In range 0-10")
}
然而,更有趣的可能是自己重寫操作符,然后使你的自定義類型可以匹配。我們假定你想重寫之前寫的士兵游戲,而且你無論如何都要使用結(jié)構(gòu)體。
struct Soldier {
let hp: Int
let x: Int
let y: Int
}
現(xiàn)在你想輕松滴匹配所有血量為 0 的實體。我們可以像下面一樣實現(xiàn) ~=
操作符。
func ~= (pattern: Int, value: Soldier) -> Bool {
return pattern == value.hp
}
現(xiàn)在我們就可以對一個實體做匹配了:
let soldier = Soldier(hp: 99, x: 10, y: 10)
switch soldier {
case 0: print("dead soldier")
default: ()
}
不幸滴是,對元組做全匹配似乎不好使。如果你實現(xiàn)下面的代碼,就會出現(xiàn)類型檢查錯誤。
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))
但是這相當(dāng)麻煩而且沒有利用好模式匹配背后的大量魔法。
在這篇博文之前的版本中我寫過 ~=
不適用于協(xié)議,但是我錯了。我記得我在一個 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
做很多事情。如果想要了解更多表達模式的細節(jié),看看這篇由 Austin Zheng 寫的超棒博文。
現(xiàn)在我們已經(jīng)講完了所有可能的 switch
模式。在我們繼續(xù)講解之前,還需要討論最后一件事情。
下面的內(nèi)容和模式匹配沒有直接關(guān)系,僅僅是和 switch
關(guān)鍵詞有關(guān),所以我就簡單說了。默認來說,和 C/C++/Objective-C不一樣的是:switch cases
不會自動進入下一個 case
,這也是為什么 Swift 不需要給每個 case
都寫上 break
。你可以選擇使用 fallthrough
關(guān)鍵詞來實現(xiàn)傳統(tǒng)的自動進入下一個 case
的行為。
switch 5 {
case 5:
print("Is 5")
fallthrough
default:
print("Is a number")
}
// 會在命令行輸出: "Is 5" "Is a number"
另外,你可以使用 break
來提前跳出 switch
語句。既然不會默認進入下一個 case
,為什么還需要這么做呢?比如你知道在一個 case
中有一個必須的要求是不滿足的,這樣你就不能繼續(xù)執(zhí)行這個 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: ()
}
... 更多你需要執(zhí)行的代碼
在這段代碼中,當(dāng) getSystemUser
返回的結(jié)果是 nil
時你不想再繼續(xù)調(diào)用 insertIntoRemoteData
。當(dāng)然,你可以在這里使用 if let
,但是如果多個這樣的情況結(jié)合到一起的時候,很快你就會得到一堆可怕丑陋的 if lets
嵌套代碼。
但是如果你是在一個 while
循環(huán)中執(zhí)行你的 switch
語句,然后你想跳出循環(huán),而不是 switch
的時候,你需要怎么做呢?對與這種情況, Swift 允許你定義一個 labels
,然后 break
或者 continue
到這個 labels
:
gameLoop: while true {
switch state() {
case .Waiting: continue gameLoop
case .Done: calculateNextState()
case .GameOver: break gameLoop
}
}
我們已經(jīng)討論過 switch
和模式匹配的語法和實現(xiàn)細節(jié)?,F(xiàn)在,讓我們來看一些有趣(多少有點)的真實案例。
對可選值進行解包的方式有很多種,模式匹配就是其中一種??赡艿浆F(xiàn)在這種方法你已經(jīng)用得非常頻繁了,但還是給一個簡短的例子吧:
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
執(zhí)行 switch
。我們可以確定它是 .None
或者是一個確定的值。更進一步,如果他是一個確定的值,我們可以在 a
這種情況下馬上把這個值綁定到一個變量。這段代碼代碼的優(yōu)美之處在于:變量 result
可能存在的兩種狀態(tài)被非常明顯滴區(qū)分開來。
做為強類型體系,Swift 通常不會像 Objective-C 那樣經(jīng)常需要運行時類型檢查。然而,當(dāng)你需要與傳統(tǒng)的 Objective-C 代碼交互時(這還沒有更新到簡單泛型的反射一文中),那你就經(jīng)常會碰到需要做類型檢查的代碼。假想你得到了一個包含 NSString
和 NSNumber
元素的數(shù)組:
let u = NSArray(array: [NSString(string: "String1"), NSNumber(int: 20), NSNumber(int: 40)])
當(dāng)你遍歷這個 NSArray
時,你永遠不知道你得到的是什么類型。然而, switch
語句可以讓你很簡單滴在這里測試這些類型:
for x in u {
switch x {
case _ as NSString:
print("string")
case _ as NSNumber:
print("number")
default:
print("Unknown types")
}
}
現(xiàn)在你正在給你當(dāng)?shù)氐母咝懛旨壍?iOS 應(yīng)用。老師想要輸入一個 0 到 100 的數(shù)值,然后得到一個相應(yīng)的等級字符(A-F)。模式匹配現(xiàn)在要來拯救你了:
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")
}
有一系列的數(shù)據(jù)對,每個數(shù)據(jù)對代表一個字和它在某段文字中出現(xiàn)的頻率。我們的目標(biāo)就是把那些低于或者高于某個固定閾值的數(shù)據(jù)對過濾掉,然后只返回剩下的不包含其頻率的所有字。
這是我們的字集:
let wordFreqs = [("k", 5), ("a", 7), ("b", 3)]
一個簡單的解決方案是使用 map
和 filter
進行建模:
let res = wordFreqs.filter({ (e) -> Bool in
if e.1 > 3 {
return true
} else {
return false
}
}).map { $0.0 }
print(res)
然而,因為 flatmap
只能返回非空元素,所以這個解決方案還有很大的改進空間。首先最重要的是,我們可以放棄使用 e.1
而利用元組來做適當(dāng)?shù)慕鈽?gòu)(你猜對了)。然后我們只需要調(diào)用一次 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)
假想你需要遍歷一個文件樹然后查找以下內(nèi)容:
guard let enumerator = NSFileManager.defaultManager().enumeratorAtPath("/customers/2014/")
else { return }
for url in enumerator {
switch (url.pathComponents, url.pathExtension) {
// customer1 和 customer2 創(chuàng)建的 “psd“文件
case (let f, "psd")
where f.contains("customer1")
|| f.contains("customer2"): print(url)
// customer2 創(chuàng)建的 “blend“文件
case (let f, "blend") where f.contains("customer2"): print(url)
// 所有的 “jpeg“文件
case (_, "jpg"): print(url)
default: ()
}
}
注意 contains
在第一個匹配就結(jié)束然后就不用遍歷完整的路徑了。同樣,模式匹配的代碼非常簡潔明了。
同樣,來看一下使用模式匹配實現(xiàn)的 fibonacci 算法有多優(yōu)美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))
當(dāng)然,如果是大數(shù)的話,程序棧會爆掉。
通常情況下,當(dāng)你從外部源取數(shù)據(jù)的時候,比如一個庫,或者一個 API,它不僅是一種很好的做法,而且通常在解析數(shù)據(jù)之前需要檢查數(shù)據(jù)的一致性。你需要確保所有的 key
都是存在的、或者數(shù)據(jù)的類型都正確、或者數(shù)組的長度滿足要求。如果不這么做就會因為bug(有的 key
沒有)而導(dǎo)致 app 崩潰(索引不存在的數(shù)組項)。而傳統(tǒng)的做法通常是嵌套 if
語句。
假想有 API 返回一條用戶信息。但是有兩種類型的用戶:系統(tǒng)用戶——如管理員或者郵政局長——和本地用戶——如 “John B“、“Bill Gates“等。因為系統(tǒng)的設(shè)計和增長,API 的使用者需要處理一些麻煩的事情:
system
和 local
用戶來自同一個 API 調(diào)用。department
這個字段,所以這個 key
可能是不存在的,而且早期的雇員從來都不需要填寫這個字段。name
數(shù)組可能包含 4 個元素(username,middlename,lastname 和 firstname)或者 2 個元素(full name,username)age
是代表用戶年齡的整型數(shù)我們的系統(tǒng)需要給這個 API 返回的所有系統(tǒng)用戶創(chuàng)建用戶賬號,賬號信息只包含如下信息:username 和 department。我們只需要 1980 年以前出生的用戶。如果沒有指定 department,就指定為 “Corp“。
func legacyAPI(id: Int) -> [String: AnyObject] {
return ["type": "system", "department": "Dark Arts", "age": 57,
"name": ["voldemort", "Tom", "Marvolo", "Riddle"]]
}
我們?yōu)榻o定的約束實現(xiàn)一個模式來進行匹配:
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")
注意這段代碼做了一個很危險的假設(shè):就是如果 name
數(shù)組元素的個數(shù)不是 2 個的話,那么它一定包含 4 個元素。如果這種假設(shè)不成立,我們獲得了包含 0 個元素的數(shù)組,這段代碼就會崩潰。
除了這一點,模式匹配向你展示了它是如何在只有一個 case
的情況下幫助你編寫干凈的代碼和簡化值的提取的。
同樣來看看我們是怎么寫緊跟在 case
之后 let
的,這樣一來就不必在每一次賦值的時候都重復(fù)寫它。
Swift 的文檔指出不是所有的模式都可以在 if
、for
或者 guard
語句中使用。然而,這個文檔似乎不是最新的。所有 7 種模式對這三個關(guān)鍵詞都有效。
我為那些感興趣的人編了一個例子要點,為每個模式和每個關(guān)鍵詞都寫了一個例子。
來看一個對三個關(guān)鍵詞使用 值綁定、元組和類型轉(zhuǎn)換模式的簡短例子:
// 到嗎編譯后只是一個關(guān)鍵詞的集合。其本身沒有任何意義
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
我們可以帶著這個想法詳細地看一看每一個關(guān)鍵詞。
到了 Swift 2.0 后,模式匹配變得更加重要,因為它被擴展到不僅可以支持 switch
,還可以支持其他的關(guān)鍵詞。比如,讓我們寫一個簡單的只返回非空元素的數(shù)組函數(shù):
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]))
關(guān)鍵詞 case
可以被 for
循環(huán)使用,就像 switch
中的 case
一樣。下面是另外一個例子。還記得我們之前說的游戲么?經(jīng)過第一次重構(gòu)之后,現(xiàn)在我們的實體系統(tǒng)看起來是這樣的:
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 一下的范圍繪制,最后我們調(diào)用渲染方法(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
而不是命令循環(huán)編程。同時,這也可以讓編譯器更清晰滴知道我們的意圖,從而就有了打通調(diào)度增強這條路的可能性。另外一個很好的體驗就是我們不需要完成滴拼寫出 Entity.EntityType.Soldier
。就算我們像上面一樣只寫 .Soldier
,Swift 也能明白我們的意圖。
另外一個支持模式匹配的關(guān)鍵詞就是新引入的 guard
關(guān)鍵詞。它允許你像 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
之前處理的,這樣可以提高代碼的可讀性
這也是 switch
和 for
的完美結(jié)合,可以把復(fù)雜的邏輯結(jié)構(gòu)封裝成易讀的格式。當(dāng)然,它不會讓邏輯變得更容易理解,但是至少會以更清晰的方式展現(xiàn)給你。特別是使用 enums
的時候。
if case
的作用和 guard case
相反。它是一種非常棒滴在分支中打開和匹配數(shù)據(jù)的方式。結(jié)合之前 guard
的例子。很顯然,我們需要一個 move
函數(shù),這個函數(shù)允許我們說一個實體在朝一個方向移動。因為我們的實體是 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)
一些限制已經(jīng)在文章中說過,比如有關(guān) Expression Patterns
的問題,看起來它似乎不能匹配 tuples
(那樣的話就真的很方便了)。在 Scala 和 Clojure 中,模式匹配在集合上同樣可用,所以你可以匹配它的頭部、尾部和部分等。4。這在 Swift 中是不支持的(盡管 Austin Zheng 在我之前鏈接的博客里差不多實現(xiàn)了這一點)
另外一種不可用的的情況是(這一點 Scala 同樣做得很好)對類或者結(jié)構(gòu)體進行解構(gòu)。Swift 允許我們定義一個 unapply
方法,這個方法做的事情大體和 init
相反。實現(xiàn)這個方法,然后就可以讓類型檢查器對類進行匹配。而在 Swift 中,它看起來就像下面一樣:
struct Imaginary {
let x: Int
let y: Int
func unapply() -> (Int, Int) {
// 實現(xiàn)這個方法之后,理論上來說實現(xiàn)了解構(gòu)變量所需的所有細節(jié)
return (self.x, self.y)
}
}
// 然后這個就會自動 unapply 然后再進行匹配
guard case let Imaginary(x, y) = anImaginaryObject else { break }
08/21/2015 結(jié)合 Reddit 上 foBrowsing 的有用反饋
guard case let
let (x, y)
替代 (let x, let y)
)08/22/2015 似乎有一些東西我沒測試好。我列舉的一些限制實際上是可用的,另外一個 Reddit 上的評論者(latrodectus)提出了一些非常有用的指正。
08/24/2015
if case
樣例,重命名了一些章節(jié)。_
不能匹配 nil
。那當(dāng)然是不對的,_
可以匹配所有的東西。(感謝 obecker)09/18/2015
<a name="1">1.可以把它當(dāng)做 shell
里面的 *
通配符</a>
<a name="2">2.我不清楚編譯器是否在對這點進行了優(yōu)化,但理論上來說,它應(yīng)該能計算出所需數(shù)據(jù)的正確位置,然后忽略 enum
的其他情況并內(nèi)聯(lián)這個地址</a>
<a name="3">3.當(dāng)然,不是 Haskell實現(xiàn)的對手: fib 0 = 0 fib 1 = 1 fib n = fib (n-1) + fib (n-2) </a>
<a name="4">4.比如:switch [1, 2, 4, 3] { case [, 2, , 3]: }</a>
更多建議: