Swift 元組高級用法和最佳實踐

作者:terhechte,原文鏈接,原文日期:2015/07/19
譯者:mmoaay;校對:lfb_CD;定稿:numbbbbb

作為 Swift 中比較少見的語法特性,元組只是占據了結構體和數組之間很小的一個位置。此外,它在 Objective-C(或者很多其他語言)中沒有相應的結構。最后,標準庫以及 Apple 示例代碼中對元組的使用也非常少??赡芩?Swift 中給人的印象就是用來做模式匹配,但我并不這么認為。

和元組相關的大部分教程都只關注三種使用場景(模式匹配、返回值和解構),且淺嘗輒止。本文會詳細介紹元組,并講解元組使用的最佳實踐,告訴你何時該用元組,何時不該用元組。同時我也會列出那些你不能用元組做的事情,免得你老是去 StackOverflow 提問。好了,進入正題。

絕對基礎

因為這部分內容你可能已經知道得七七八八了,所以我就簡單介紹下。

元組允許你把不同類型的數據結合到一起。它是可變的,盡管看起來像序列,但是它不是,因為不能直接遍歷所有內容。我們首先通過一個簡單的入門示例來學習如何創建和使用元組。

創建和訪問元組

// 創建一個簡單的元組
let tp1 = (2, 3)
let tp2 = (2, 3, 4)

//創建一個命名元組
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 語句提供了一種極強大的方法,可以在不搞亂源代碼的情況下簡單的定義復雜條件句。這樣就可以在一個語句中匹配類型、實例以及多個變量的值:

// 特意造出來的例子
// 這些是多個方法的返回值
let age = 23
let job: String? = "Operator"
let payload: AnyObject = NSDictionary()

在上面的代碼中,我們想要找一個 30 歲以下的工作者和一個字典 payload。假設這個 payload 是 Objective-C 世界中的一些東西,它可能是字典、數組或者數字。現在你不得不和下面這段別人很多年前寫的爛代碼打交道:

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

switch 的參數構建為元組 (age, job, payload),我們就可以用精心設計的約束條件來一次性訪問元組中所有特定或不特定的屬性。

把元組做為返回類型

這可能是元組第二多的應用場景。因為元組可以即時構建,它成了在方法中返回多個值的一種簡單有效的方式。

func abc() -> (Int, Int, String) {
    return (3, 5, "Carl")
}

元組解構

Swift 從不同的編程語言汲取了很多靈感,這也是 Python 做了很多年的事情。之前的例子大多只展示了如何把東西塞到元組中,解構則是一種迅速把東西從元組中取出的方式,結合上面的 abc 例子,我們寫出如下代碼:

let (a, b, c) = abc()
print(a)

另外一個例子是把多個方法調用寫在一行代碼中:

let (a, b, c) = (a(), b(), c())

或者,簡單的交換兩個值:

var a = 5
var b = 4
(b, a) = (a, b)

進階

元組做為匿名結構體

元組和結構體一樣允許你把不同的類型結合到一個類型中:

struct User {
  let name: String
  let age: Int
}
// vs.
let user = (name: "Carl", age: 40)

正如你所見,這兩個類型很像,只是結構體通過結構體描述聲明,聲明之后就可以用這個結構體來定義實例,而元組僅僅是一個實例。如果需要在一個方法或者函數中定義臨時結構體,就可以利用這種相似性。就像 Swift 文檔中所說:

“需要臨時組合一些相關值的時候,元組非常有用。(…)如果數據結構需要在臨時范圍之外仍然存在。那就把它抽象成類或者結構體(…)”

下面來看一個例子:需要收集多個方法的返回值,去重并插入到數據集中:

func zipForUser(userid: String) -> String { return "12124" }
func streetForUser(userid: String) -> String { return "Charles Street" }


// 從數據集中找出所有不重復的街道
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)

這里,我們在短暫的臨時場景中使用結構簡單的元組。當然也可以定義結構體,但是這并不是必須的。

再看另外一個例子:在處理算法數據的類中,你需要把某個方法返回的臨時結果傳入到另外一個方法中。定義一個只有兩三個方法會用的結構體顯然是不必要的。

// 編造算法
func calculateInterim(values: [Int]) -> (r: Int, alpha: CGFloat, chi: (CGFloat, CGFLoat)) {
   ...
}
func expandInterim(interim: (r: Int, alpha: CGFloat, chi: (CGFloat, CGFLoat))) -> CGFloat {
   ...
}

顯然,這行代碼非常優雅。單獨為一個實例定義結構體有時候過于復雜,而定義同一個元組 4 次卻不使用結構體也同樣不可取。所以選擇哪種方式取決于各種各樣的因素。

私有狀態

除了之前的例子,元組還有一種非常實用的場景:在臨時范圍以外使用。Rich Hickey 說過:“如果樹林中有一棵樹倒了,會發出聲音么?“因為作用域是私有的,元組只在當前的實現方法中有效。使用元組可以很好的存儲內部狀態。

來看一個簡單的例子:保存一個靜態的 UITableView 結構,這個結構用來展示用戶簡介中的各種信息以及信息對應值的 keypath,同時還用editable標識表示點擊 Cell 時是否可以對這些值進行編輯。

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)]

另一種選擇就是定義結構體,但是如果數據的實現細節是純私有的,用元組就夠了。

更酷的一個例子是:你定義了一個對象,并且想給這個對象添加多個變化監聽器,每個監聽器都包含它的名字以及發生變化時被調用的閉包:

func addListener(name: String, action: (change: AnyObject?) -> ())
func removeListener(name: String)

你會如何在對象中保存這些監聽器呢?顯而易見的解決方案是定義一個結構體,但是這些監聽器只能在三種情況下用,也就是說它們使用范圍極其有限,而結構體只能定義為 internal ,所以,使用元組可能會是更好的解決方案,因為它的解構能力會讓事情變得很簡單:

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 方法中看到的一樣,元組的解構能力讓它在這種情況下特別好用,因為內容都是在局部作用域中直接解構。

把元組作為固定大小的序列

元組的另外一個應用領域是:固定一個類型所包含元素的個數。假設需要用一個對象來計算一年中所有月份的各種統計值,你需要分開給每個月份存儲一個確定的 Integer 值。首先能想到的解決方案會是這樣:

var monthValues: [Int]

然而,這樣的話我們就不能確定這個屬性剛好包含 12 個元素。使用這個對象的用戶可能不小心插入了 13 個值,或者 11 個。我們沒法告訴類型檢查器這個對象是固定 12 個元素的數組(有意思的是,這是 C 都支持的事情)。但是如果使用元組,可以很簡單地實現這種特殊的約束:

var monthValues: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int)

還有一種選擇就是在對象的功能中加入約束邏輯(即通過新的 guard 語句),然而這個是在運行時檢查。元組的檢查則是在編譯期間;當你想給對象賦值 11 個月時,編譯都通不過。

元組當做復雜的可變參數類型

可變參數(比如可變函數參數)是在函數參數的個數不定的情況下非常有用的一種技術。

// 傳統例子
func sumOf(numbers: Int...) -> Int {
    // 使用 + 操作符把所有數字加起來
    return numbers.reduce(0, combine: +)
}

sumOf(1, 2, 5, 7, 9) // 24

如果你的需求不單單是 integer,元組就會變的很有用。下面這個函數做的事情就是批量更新數據庫中的 n 個實體:

func batchUpdate(updates: (String, Int)...) -> Bool {
    self.db.begin()
    for (key, value) in updates {
    self.db.set(key, value)
    }
    self.db.end()
}

// 我們假想數據庫是很復雜的
batchUpdate(("tk1", 5), ("tk7", 9), ("tk21", 44), ("tk88", 12))

高級用法

元組迭代

在之前的內容中,我試圖避免把元組叫做序列或者集合,因為它確實不是。因為元組中每個元素都可以是不同的類型,所以無法使用類型安全的方式對元組的內容進行遍歷或者映射?;蛘哒f至少沒有優雅的方式。

Swift 提供了有限的反射能力,這就允許我們檢查元組的內容然后對它進行遍歷。不好的地方就是類型檢查器不知道如何確定遍歷元素的類型,所以所有內容的類型都是 Any。你需要自己轉換和匹配那些可能有用的類型并決定要對它們做什么。

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: ()
    }
}

這當然沒有數組迭代那么簡單,但是如果確實需要,可以使用這段代碼。

元組和泛型

Swift 中并沒有 Tuple 這個類型。如果你不知道為什么,可以這樣想:每個元組都是完全不同的類型,它的類型取決于它包含元素的類型。

所以,與其定義一個支持泛型的元組,還不如根據自己需求定義一個包含具體數據類型的元組。

func wantsTuple<T1, T2>(tuple: (T1, T2)) -> T1 {
    return tuple.0
}

wantsTuple(("a", "b")) // "a"
wantsTuple((1, 2)) // 1

你也可以通過 typealiases 使用元組,從而允許子類指定具體的類型。這看起來相當復雜而且無用,但是我已經碰到了需要特意這樣做的使用場景。

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, "")

定義具體的元組類型

在之前好幾個例子中,我們多次重復一些已經確定的類型,比如 (Int, Int, String)。這當然不需要每次都寫,你可以為它定義一個 typealias

typealias Example = (Int, Int, String)
func add(elm: Example) {
}

但是,如果需要如此頻繁的使用一個確定的元組結構,以至于你想給它增加一個 typealias,那么最好的方式是定義一個結構體。

用元組做函數參數

就像 Paul Robinson 的文章 中說到的一樣,(a: Int, b: Int, c: String) ->(a: Int, b: Int, c:String) 之間有一種奇妙的相似。確實,對于 Swift 的編譯器而言,方法/函數的參數頭無非就是一個元組:

// 從 Paul Robinson 的博客拷貝來的, 你也應該去讀讀這篇文章:
// 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

這看起來很酷是不是?但是等等…這里的函數簽名有點特殊。當我們像元組一樣增加或者移除標簽的時候會發生什么呢?哦了,我們現在開始實驗:

// 讓我們試一下帶標簽的:
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)

所以如果函數簽名帶標簽的話就可以支持帶標簽的元組。

但我們是否需要明確的把元組寫入到變量中呢?

foo2((a: 4, b: 3, name: "hello")) // 出錯

好吧,比較倒霉,上面的代碼是不行的,但是如果是通過調用函數返回的元組呢?

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!

太棒了!這種方式可以!

這種方式包含了很多有趣的含義和可能性。如果對類型進行很好的規劃,你甚至可以不需要對數據進行解構,然后直接把它們當作參數在函數間傳遞。

更妙的是,對于函數式編程,你可以直接返回一個含多個參數的元組到一個函數中,而不需要對它進行解構。

元組做不到啊~

最后,我們把一些元組不能實現事情以列表的方式呈現給大家。

用元組做字典的 Key

如果你想做如下的事情:

let p: [(Int, Int): String]

那是不可能的,因為元組不符合哈希協議。這真是一件令人傷心的事,因為這種寫法有很多應用場景??赡軙携偪竦念愋蜋z查器黑客對元組進行擴展以使它符合哈希協議,但是我還真的沒有研究過這個,所以如果你剛好發現這是可用的,請隨時通過我的 twitter 聯系我。

元組的協議合規性

給定如下的協議:

protocol PointProtocol {
  var x: Int { get }
  var y: Int { set }
}

你沒法告訴類型檢查器這個 (x: 10, y: 20) 元組符合這個協議。

func addPoint(point: PointProtocol)
addPoint((x: 10, y: 20)) // 不可用。

附錄

就這樣了。如果我忘了說或者說錯一些事情,如果你發現了確切的錯誤,或者有一些其他我忘了的事情,請隨時聯系我

更新

07/23/2015 添加用元組做函數參數章節

08/06/2015 更新反射例子到最新的 Swift beta 4(移除了對 reflect 的調用)

08/12/2015 更新用元組做函數參數章節,加入更多的例子和信息

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容