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