作為 Swift 中比較少見的語法特性,元組只是占據(jù)了結(jié)構(gòu)體和數(shù)組之間很小的一個位置。此外,它在 Objective-C(或者很多其他語言)中沒有相應(yīng)的結(jié)構(gòu)。最后,標(biāo)準(zhǔn)庫以及 Apple 示例代碼中對元組的使用也非常少??赡芩?Swift 中給人的印象就是用來做模式匹配,但我并不這么認(rèn)為。
和元組相關(guān)的大部分教程都只關(guān)注三種使用場景(模式匹配、返回值和解構(gòu)),且淺嘗輒止。本文會詳細(xì)介紹元組,并講解元組使用的最佳實踐,告訴你何時該用元組,何時不該用元組。同時我也會列出那些你不能用元組做的事情,免得你老是去 StackOverflow 提問。好了,進(jìn)入正題。
因為這部分內(nèi)容你可能已經(jīng)知道得七七八八了,所以我就簡單介紹下。
元組允許你把不同類型的數(shù)據(jù)結(jié)合到一起。它是可變的,盡管看起來像序列,但是它不是,因為不能直接遍歷所有內(nèi)容。我們首先通過一個簡單的入門示例來學(xué)習(xí)如何創(chuàng)建和使用元組。
// 創(chuàng)建一個簡單的元組
let tp1 = (2, 3)
let tp2 = (2, 3, 4)
//創(chuàng)建一個命名元組
let tp3 = (x: 5, y: 3)
// 不同的類型
let tp4 = (name: "Carl", age: 78, pets: ["Bonny", "Houdon", "Miki"])
// 訪問元組元素
let tp5 = (13, 21)
tp5.0 // 13
tp5.1 // 21
let tp6 = (x: 21, y: 33)
tp6.x // 21
tp6.y // 33
就像之前所說,這大概是元組最常見的使用場景。Swift 的 switch
語句提供了一種極強(qiáng)大的方法,可以在不搞亂源代碼的情況下簡單的定義復(fù)雜條件句。這樣就可以在一個語句中匹配類型、實例以及多個變量的值:
// 特意造出來的例子
// 這些是多個方法的返回值
let age = 23
let job: String? = "Operator"
let payload: AnyObject = NSDictionary()
在上面的代碼中,我們想要找一個 30 歲以下的工作者和一個字典 payload
。假設(shè)這個 payload
是 Objective-C 世界中的一些東西,它可能是字典、數(shù)組或者數(shù)字?,F(xiàn)在你不得不和下面這段別人很多年前寫的爛代碼打交道:
switch (age, job, payload) {
case (let age, _?, _ as NSDictionary) where age < 30:
print(age)
default: ()
}
把 switch
的參數(shù)構(gòu)建為元組 (age, job, payload)
,我們就可以用精心設(shè)計的約束條件來一次性訪問元組中所有特定或不特定的屬性。
這可能是元組第二多的應(yīng)用場景。因為元組可以即時構(gòu)建,它成了在方法中返回多個值的一種簡單有效的方式。
func abc() -> (Int, Int, String) {
return (3, 5, "Carl")
}
Swift 從不同的編程語言汲取了很多靈感,這也是 Python 做了很多年的事情。之前的例子大多只展示了如何把東西塞到元組中,解構(gòu)則是一種迅速把東西從元組中取出的方式,結(jié)合上面的 abc
例子,我們寫出如下代碼:
let (a, b, c) = abc()
print(a)
另外一個例子是把多個方法調(diào)用寫在一行代碼中:
let (a, b, c) = (a(), b(), c())
或者,簡單的交換兩個值:
var a = 5
var b = 4
(b, a) = (a, b)
元組和結(jié)構(gòu)體一樣允許你把不同的類型結(jié)合到一個類型中:
struct User {
let name: String
let age: Int
}
// vs.
let user = (name: "Carl", age: 40)
正如你所見,這兩個類型很像,只是結(jié)構(gòu)體通過結(jié)構(gòu)體描述聲明,聲明之后就可以用這個結(jié)構(gòu)體來定義實例,而元組僅僅是一個實例。如果需要在一個方法或者函數(shù)中定義臨時結(jié)構(gòu)體,就可以利用這種相似性。就像 Swift 文檔中所說:
“需要臨時組合一些相關(guān)值的時候,元組非常有用。(…)如果數(shù)據(jù)結(jié)構(gòu)需要在臨時范圍之外仍然存在。那就把它抽象成類或者結(jié)構(gòu)體(…)”
下面來看一個例子:需要收集多個方法的返回值,去重并插入到數(shù)據(jù)集中:
func zipForUser(userid: String) -> String { return "12124" }
func streetForUser(userid: String) -> String { return "Charles Street" }
// 從數(shù)據(jù)集中找出所有不重復(fù)的街道
var streets: [String: (zip: String, street: String, count: Int)] = [:]
for userid in users {
let zip = zipForUser(userid)
let street = streetForUser(userid)
let key = "\(zip)-\(street)"
if let (_, _, count) = streets[key] {
streets[key] = (zip, street, count + 1)
} else {
streets[key] = (zip, street, 1)
}
}
drawStreetsOnMap(streets.values)
這里,我們在短暫的臨時場景中使用結(jié)構(gòu)簡單的元組。當(dāng)然也可以定義結(jié)構(gòu)體,但是這并不是必須的。
再看另外一個例子:在處理算法數(shù)據(jù)的類中,你需要把某個方法返回的臨時結(jié)果傳入到另外一個方法中。定義一個只有兩三個方法會用的結(jié)構(gòu)體顯然是不必要的。
// 編造算法
func calculateInterim(values: [Int]) -> (r: Int, alpha: CGFloat, chi: (CGFloat, CGFLoat)) {
...
}
func expandInterim(interim: (r: Int, alpha: CGFloat, chi: (CGFloat, CGFLoat))) -> CGFloat {
...
}
顯然,這行代碼非常優(yōu)雅。單獨為一個實例定義結(jié)構(gòu)體有時候過于復(fù)雜,而定義同一個元組 4 次卻不使用結(jié)構(gòu)體也同樣不可取。所以選擇哪種方式取決于各種各樣的因素。
除了之前的例子,元組還有一種非常實用的場景:在臨時范圍以外使用。Rich Hickey 說過:“如果樹林中有一棵樹倒了,會發(fā)出聲音么?“因為作用域是私有的,元組只在當(dāng)前的實現(xiàn)方法中有效。使用元組可以很好的存儲內(nèi)部狀態(tài)。
來看一個簡單的例子:保存一個靜態(tài)的 UITableView
結(jié)構(gòu),這個結(jié)構(gòu)用來展示用戶簡介中的各種信息以及信息對應(yīng)值的 keypath
,同時還用editable
標(biāo)識表示點擊 Cell
時是否可以對這些值進(jìn)行編輯。
let tableViewValues = [(title: "Age", value: "user.age", editable: true),
(title: "Name", value: "user.name.combinedName", editable: true),
(title: "Username", value: "user.name.username", editable: false),
(title: "ProfilePicture", value: "user.pictures.thumbnail", editable: false)]
另一種選擇就是定義結(jié)構(gòu)體,但是如果數(shù)據(jù)的實現(xiàn)細(xì)節(jié)是純私有的,用元組就夠了。
更酷的一個例子是:你定義了一個對象,并且想給這個對象添加多個變化監(jiān)聽器,每個監(jiān)聽器都包含它的名字以及發(fā)生變化時被調(diào)用的閉包:
func addListener(name: String, action: (change: AnyObject?) -> ())
func removeListener(name: String)
你會如何在對象中保存這些監(jiān)聽器呢?顯而易見的解決方案是定義一個結(jié)構(gòu)體,但是這些監(jiān)聽器只能在三種情況下用,也就是說它們使用范圍極其有限,而結(jié)構(gòu)體只能定義為 internal
,所以,使用元組可能會是更好的解決方案,因為它的解構(gòu)能力會讓事情變得很簡單:
var listeners: [(String, (AnyObject?) -> ())]
func addListener(name: String, action: (change: AnyObject?) -> ()) {
self.listeners.append((name, action))
}
func removeListener(name: String) {
if let idx = listeners.indexOf({ e in return e.0 == name }) {
listeners.removeAtIndex(idx)
}
}
func execute(change: Int) {
for (_, listener) in listeners {
listener(change)
}
}
就像你在 execute
方法中看到的一樣,元組的解構(gòu)能力讓它在這種情況下特別好用,因為內(nèi)容都是在局部作用域中直接解構(gòu)。
元組的另外一個應(yīng)用領(lǐng)域是:固定一個類型所包含元素的個數(shù)。假設(shè)需要用一個對象來計算一年中所有月份的各種統(tǒng)計值,你需要分開給每個月份存儲一個確定的 Integer
值。首先能想到的解決方案會是這樣:
var monthValues: [Int]
然而,這樣的話我們就不能確定這個屬性剛好包含 12 個元素。使用這個對象的用戶可能不小心插入了 13 個值,或者 11 個。我們沒法告訴類型檢查器這個對象是固定 12 個元素的數(shù)組(有意思的是,這是 C 都支持的事情)。但是如果使用元組,可以很簡單地實現(xiàn)這種特殊的約束:
var monthValues: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int)
還有一種選擇就是在對象的功能中加入約束邏輯(即通過新的 guard
語句),然而這個是在運(yùn)行時檢查。元組的檢查則是在編譯期間;當(dāng)你想給對象賦值 11 個月時,編譯都通不過。
可變參數(shù)(比如可變函數(shù)參數(shù))是在函數(shù)參數(shù)的個數(shù)不定的情況下非常有用的一種技術(shù)。
// 傳統(tǒng)例子
func sumOf(numbers: Int...) -> Int {
// 使用 + 操作符把所有數(shù)字加起來
return numbers.reduce(0, combine: +)
}
sumOf(1, 2, 5, 7, 9) // 24
如果你的需求不單單是 integer
,元組就會變的很有用。下面這個函數(shù)做的事情就是批量更新數(shù)據(jù)庫中的 n
個實體:
func batchUpdate(updates: (String, Int)...) -> Bool {
self.db.begin()
for (key, value) in updates {
self.db.set(key, value)
}
self.db.end()
}
// 我們假想數(shù)據(jù)庫是很復(fù)雜的
batchUpdate(("tk1", 5), ("tk7", 9), ("tk21", 44), ("tk88", 12))
在之前的內(nèi)容中,我試圖避免把元組叫做序列或者集合,因為它確實不是。因為元組中每個元素都可以是不同的類型,所以無法使用類型安全的方式對元組的內(nèi)容進(jìn)行遍歷或者映射?;蛘哒f至少沒有優(yōu)雅的方式。
Swift 提供了有限的反射能力,這就允許我們檢查元組的內(nèi)容然后對它進(jìn)行遍歷。不好的地方就是類型檢查器不知道如何確定遍歷元素的類型,所以所有內(nèi)容的類型都是 Any
。你需要自己轉(zhuǎn)換和匹配那些可能有用的類型并決定要對它們做什么。
let t = (a: 5, b: "String", c: NSDate())
let mirror = Mirror(reflecting: t)
for (label, value) in mirror.children {
switch value {
case is Int:
print("int")
case is String:
print("string")
case is NSDate:
print("nsdate")
default: ()
}
}
這當(dāng)然沒有數(shù)組迭代那么簡單,但是如果確實需要,可以使用這段代碼。
Swift 中并沒有 Tuple
這個類型。如果你不知道為什么,可以這樣想:每個元組都是完全不同的類型,它的類型取決于它包含元素的類型。
所以,與其定義一個支持泛型的元組,還不如根據(jù)自己需求定義一個包含具體數(shù)據(jù)類型的元組。
func wantsTuple<T1, T2>(tuple: (T1, T2)) -> T1 {
return tuple.0
}
wantsTuple(("a", "b")) // "a"
wantsTuple((1, 2)) // 1
你也可以通過 typealiases
使用元組,從而允許子類指定具體的類型。這看起來相當(dāng)復(fù)雜而且無用,但是我已經(jīng)碰到了需要特意這樣做的使用場景。
class BaseClass<A,B> {
typealias Element = (A, B)
func addElement(elm: Element) {
print(elm)
}
}
class IntegerClass<B> : BaseClass<Int, B> {
}
let example = IntegerClass<String>()
example.addElement((5, ""))
// Prints (5, "")
在之前好幾個例子中,我們多次重復(fù)一些已經(jīng)確定的類型,比如 (Int, Int, String)
。這當(dāng)然不需要每次都寫,你可以為它定義一個 typealias
:
typealias Example = (Int, Int, String)
func add(elm: Example) {
}
但是,如果需要如此頻繁的使用一個確定的元組結(jié)構(gòu),以至于你想給它增加一個 typealias
,那么最好的方式是定義一個結(jié)構(gòu)體。
就像 Paul Robinson 的文章 中說到的一樣,(a: Int, b: Int, c: String) ->
和 (a: Int, b: Int, c:String)
之間有一種奇妙的相似。確實,對于 Swift 的編譯器而言,方法/函數(shù)的參數(shù)頭無非就是一個元組:
// 從 Paul Robinson 的博客拷貝來的, 你也應(yīng)該去讀讀這篇文章:
// http://www.paulrobinson.net/function-parameters-are-tuples-in-swift/
func foo(a: Int, _ b: Int, _ name: String) -> Int
return a
}
let arguments = (4, 3, "hello")
foo(arguments) // 返回 4
這看起來很酷是不是?但是等等…這里的函數(shù)簽名有點特殊。當(dāng)我們像元組一樣增加或者移除標(biāo)簽的時候會發(fā)生什么呢?哦了,我們現(xiàn)在開始實驗:
// 讓我們試一下帶標(biāo)簽的:
func foo2(a a: Int, b: Int, name: String) -> Int {
return a
}
let arguments = (4, 3, "hello")
foo2(arguments) // 不能用
let arguments2 = (a: 4, b: 3, name: "hello")
foo2(arguments2) // 可以用 (4)
所以如果函數(shù)簽名帶標(biāo)簽的話就可以支持帶標(biāo)簽的元組。
但我們是否需要明確的把元組寫入到變量中呢?
foo2((a: 4, b: 3, name: "hello")) // 出錯
好吧,比較倒霉,上面的代碼是不行的,但是如果是通過調(diào)用函數(shù)返回的元組呢?
func foo(a: Int, _ b: Int, _ name: String) -> Int
return a
}
func get_tuple() -> (Int, Int, String) {
return (4, 4, "hello")
}
foo(get_tuple()) // 可以用! 返回 4!
太棒了!這種方式可以!
這種方式包含了很多有趣的含義和可能性。如果對類型進(jìn)行很好的規(guī)劃,你甚至可以不需要對數(shù)據(jù)進(jìn)行解構(gòu),然后直接把它們當(dāng)作參數(shù)在函數(shù)間傳遞。
更妙的是,對于函數(shù)式編程,你可以直接返回一個含多個參數(shù)的元組到一個函數(shù)中,而不需要對它進(jìn)行解構(gòu)。
最后,我們把一些元組不能實現(xiàn)事情以列表的方式呈現(xiàn)給大家。
Key
如果你想做如下的事情:
let p: [(Int, Int): String]
那是不可能的,因為元組不符合哈希協(xié)議。這真是一件令人傷心的事,因為這種寫法有很多應(yīng)用場景。可能會有瘋狂的類型檢查器黑客對元組進(jìn)行擴(kuò)展以使它符合哈希協(xié)議,但是我還真的沒有研究過這個,所以如果你剛好發(fā)現(xiàn)這是可用的,請隨時通過我的 twitter 聯(lián)系我。
給定如下的協(xié)議:
protocol PointProtocol {
var x: Int { get }
var y: Int { set }
}
你沒法告訴類型檢查器這個 (x: 10, y: 20)
元組符合這個協(xié)議。
func addPoint(point: PointProtocol)
addPoint((x: 10, y: 20)) // 不可用。
就這樣了。如果我忘了說或者說錯一些事情,如果你發(fā)現(xiàn)了確切的錯誤,或者有一些其他我忘了的事情,請隨時聯(lián)系我
07/23/2015 添加用元組做函數(shù)參數(shù)章節(jié)
08/06/2015 更新反射例子到最新的 Swift beta 4(移除了對 reflect
的調(diào)用)
08/12/2015 更新用元組做函數(shù)參數(shù)章節(jié),加入更多的例子和信息
08/13/2015 修復(fù)了一些bug…
更多建議: