模式匹配第一彈: switch, enums & where 子句
從簡單的 switch 到復雜的表達式、Swift 中的模式匹配可以變得相當強大。今天開始我們來探索一下 switch 炫酷的使用技巧,稍后一系列文章會更進一步,為大家帶來更高級的模式匹配技法。
本文作為模式匹配的第一篇介紹文章,旨在拋磚引玉。
Switch 基本用法
Swift 中最簡單、最為常見的模式匹配就是 switch 語句,大家對下面的形式都比較熟悉了:
enum Direction {
case North, South, East, West
}
// 可以很容易地切換枚舉值
extension Direction: CustomStringConvertible {
var description: String {
switch self {
case North: return "↑"
case South: return "↓"
case East: return "→"
case West: return "←"
}
}
}
但是 switch 可以更進一步,允許使用包含變量的匹配模式,并在匹配時綁定這些變量。這適用于帶關聯值的枚舉對象:
enum Media {
case Book(title: String, author: String, year: Int)
case Movie(title: String, director: String, year: Int)
case WebSite(url: NSURL, title: String)
}
extension Media {
var mediaTitle: String {
switch self {
case .Book(title: let aTitle, author: _, year: _):
return aTitle
case .Movie(title: let aTitle, director: _, year: _):
return aTitle
case .WebSite(url: _, title: let aTitle):
return aTitle
}
}
}
let book = Media.Book(title: "20,000 leagues under the sea", author: "Jules Verne", year: 1870)
book.mediaTitle
這種形式下的通用語法是 case MyEnum.EnumValue(let variable) ,表明『如果對應的值是 MyEnum.EnumValue 且附帶一個關聯值,就綁定變量 variable 到該關聯值上』。
當 media 實例匹配其中一個條件,比如匹配第一個 case: Media.Book ,緊接著一個新的變量 let aTitle 被創建,且相關聯的值 title 也會綁定到這個變量中。這就是我們為什么這里需要一個 let ,因為如果匹配的話會創建一個新的變量(或常量)
請注意,你可以將 let 寫在整個表達式的前面,而不用再寫在每個變量前,下面這兩行代碼是等價的:
case .Book(title: let aTitle, author: let anAuthor, year: let aYear): …
case let .Book(title: aTitle, author: anAuthor, year: aYear): …
上面的代碼使用了通配符 _ ,意味著:『我期待這里有東西就好,但不關心是什么,所以在我不使用時,不要將其綁定到一個變量』,類似于我們為不使用的值放置一個占位符。
使用固定的值
記住 case 仍然是關于模式匹配的,與之相比較的他都會嘗試去匹配一下。這意味著你可以使用一個常量值來判斷是否匹配,例如:
extension Media {
var isFromJulesVerne: Bool {
switch self {
case .Book(title: _, author: "Jules Verne", year: _): return true
case .Movie(title: _, director: "Jules Verne", year: _): return true
default: return false
}
}
}
book.isFromJulesVerne
這個例子或許并沒有什么實際意義,但是至少向你展示了如何綁定常量。我提到它是因為我們已經學習過將一個值綁定到一個變量上,然后判斷該變量是否等于一個常量,而不是直接和這個常量進行模式匹配。
一個更有幫助和通用的例子看起來應該是這樣的:
extension Media {
func checkAuthor(author: String) -> Bool {
switch self {
case .Book(title: _, author: author, year: _): return true
case .Movie(title: _, director: author, year: _): return true
default: return false
}
}
}
book.checkAuthor("Jules Verne")
注意這里盡管我們在 case 模式中使用了 author ,但這里不需要使用 let (與之前的代碼不同)。因為這種情況下,我們不會創建一個變量來綁定一個值。相反,我們使用一個已經被賦過值的常量(這個值由函數參數提供),而不會創建一個新值綁定到匹配的 self 上
一次綁定多種模式
在 Swift 2.2 中,我們不能一次綁定多種模式,所以下面的例子是不能正常工作的,因為我們嘗試聲明了一些變量用做 self 匹配 .Book 或 .Movie 的情形,然后只要滿足這兩種情形任意一種就綁定一個變量:
extension Media {
var mediaTitle2: String {
switch self {
// 錯誤: 'case' 標簽中含多種模式,變量是不能被聲明的
case let .Book(title: aTitle, author: _, year: _), let .Movie(title: aTitle, director: _, year: _):
return aTitle
case let .WebSite(url: _, title: aTitle):
return aTitle
}
}
}
這在大多數情形下是可以理解的;你期望下面這些代碼做些什么:
case let .Book(title: aTitle, author: _, year: _), let .Movie(title: _, director: _, year: aYear)?
隨后你該如何在代碼中使用綁定的變量( aTitle 或 aYear )呢?如果 self 是 .Book ,則只有 aTitle 將會被綁定,那么 aYear 怎么辦?如果你在隨后的 case 下的代碼塊中使用 aYear 變量,不可能有意義的。
但是可能需要考慮的一個特例是當你嘗試綁定相同名字和類型的變量時,這仍然是有意義的工作,就像上面例子中我們嘗試在 cases 是 .Book 和 .Movie 時綁定 aTitle 。這樣做可以避免重復代碼,不是嗎?所以為什么在那種情形下還不允許這么用?別擔心,這個 提議 已經被 Swift 3.0 接受了。
使用沒有參數標簽的元組
注意當處理 enum 的關聯值時,我們可以考慮使用一個元組來做唯一的關聯值,而元組則包含所有真正的關聯值。下面有兩個結果:
- 第一個你可以省略參數標簽,然后 case 仍然能夠正常工作:
extension Media {
var mediaTitle2: String {
switch self {
case let .Book(title, _, _): return title
case let .Movie(title, _, _): return title
case let .WebSite(_, title): return title
}
}
}
- 第二個你可以將關聯值看做是一個唯一的大元組,然后方法里面獨立的元素。
extension Media {
var mediaTitle3: String {
switch self {
case let .Book(tuple): return tuple.title
case let .Movie(tuple): return tuple.title
case let .WebSite(tuple): return tuple.title
}
}
}
作為附加的獎勵,可以不用指定特定的元組來匹配任何關聯值,所以下面三個表達式是相等的:
case .WebSite // not specifying the tuple at all
case .WebSite(_) // matching a single tuple of associated values that we don't care about
case .WebSite(_, _) // matching individual associated values that we don't care about either
使用 Where 語句
比起比較兩個枚舉,模式匹配允許更強大的方式,你可以在比較中添加條件,使用 where 從句,如下:
extension Media {
var publishedAfter1930: Bool {
switch self {
case let .Book(_, _, year) where year > 1930: return true
case let .Movie(_, _, year) where year > 1930: return true
case .WebSite: return true // same as "case .WebSite(_)" but we ignore the associated tuple value
default: return false
}
}
}
這樣只有當左邊的模式( let .Book(_, _, year) )成功匹配,并且 where 限定的條件為 true 時,整個匹配過程才算完成。在稍后的系列文章中我們將繼續深挖更強大的模式匹配。
下一部分的計劃
這篇文章很簡單,帶你回顧了 swith 中的一些基本的模式匹配,下一部分將探討更高級的用法,包括:
- 在 enum 之外的其他地方使用 switch (尤其指 tuples , structs , is 和 as )
- 與其他語句一起配合使用模式匹配,包括 if case , guard case , for case , =~ , …
- 嵌套模式,包括包含可選值
- 將上面這些結合起來創造一些魔法