如果你曾經(jīng)使用過 Objective-C 或者像 Ruby,Python,JavaScript 這樣的語言,可能會覺得 Swift 里的結構體就像外星人一樣奇異。類是面向對象編程語言中傳統(tǒng)的結構單元。的確,和結構體相比,Swift 的類支持實現(xiàn)繼承,(受限的)反射,析構函數(shù)和多所有者。
既然類比結構體強大這么多,為什么還要使用結構體?正是因為它的使用范圍受限,使得結構體在構建代碼塊 (blocks) 的時候非常靈活。在本文中,你將會學習到結構體和其他的值類型是如何大幅提高代碼的清晰度、靈活性和可讀性的。
結構體是值類型的,而類是引用類型的,這一行為上的細微區(qū)別造就了架構上的無限可能。
值類型的實例,不管是在賦值或是作為函數(shù)參數(shù)的時候,都是被復制的。數(shù)字,字符串,數(shù)組,字典,枚舉,元組和結構體都是值類型。比如:
var a = "Hello"
var b = a
b.extend(", world")
println("a: \(a); b: \(b)") // a: Hello; b: Hello, world
引用類型的實例 (主要是類) 可以有多個所有者。將一個引用賦值給一個新的變量或者傳遞給一個函數(shù)的時候,它們都指向同一個實例。這是你熟悉的對象的行為。比如:
var a = UIView()
var b = a
b.alpha = 0.5
println("a: \(a.alpha); b: \(b.alpha)") // a: 0.5; b: 0.5
這兩種類型的區(qū)別看起來似乎不大,但是選擇值類型還是選擇引用類型會給你的系統(tǒng)架構帶來很大的差異。
既然我們已經(jīng)知道了值類型和引用類型行為上的區(qū)別,現(xiàn)在讓我們討論一下使用上的區(qū)別。
Swift 將來除了對象可能還會有其他的引用類型,但是就這次討論,我們只將對象作為引用類型的范例。
我們在代碼中引用對象和我們在現(xiàn)實生活中引用對象是一樣的。編程書籍經(jīng)常使用一個現(xiàn)實世界的隱喻來教授人們面向對象編程:你可以創(chuàng)建一個 Dog
類,然后將它實例化來定義 fido
(譯注:狗的名字)。如果你將 fido
在系統(tǒng)的不同部分之間傳遞,它們談論的仍然是同一個 fido
。這是有意義的,因為如果你的確有一只叫 Fido 的狗,無論何時你談到它時,你將會使用它的名字進行信息傳輸 —— 而不是傳輸狗本身。你可能依賴于其他人知道 Fido 是誰。當你使用對象的時候,你是在系統(tǒng)內傳遞著實例的名字
。
值就像數(shù)據(jù)一樣。如果你向別人發(fā)出了一張費用開銷表,你發(fā)出的不是一個代表那個信息的標簽 —— 你是在傳遞信息本身。消息接收者可以在不和任何人交流的情況下,計算總和,或者把費用寫下來供日后查閱。如果消息接收者打印了費用表并且修改了它們,這也沒有修改你自己的那張表。
一個值可以是一個數(shù)字,也許代表一個價格,或是一個類似字符串的描述。它可以是枚舉中的一個選項:這次的花費是因為一頓晚餐,還是旅行,還是材料?在指定的位置中還能包括一些其他的值,比如一個代表經(jīng)度和緯度的 CLLocationCoordinate2D
結構體?;蛘咚梢允且恍┢渌档牧斜淼鹊取?/p>
Fido 可能在自己的地盤里來回跑叫。它也許會有特殊的行為使它區(qū)別于其他的狗。他可能會同其他的狗建立關系。你不能把 Fido 換成其他的狗 —— 你的孩子們會發(fā)現(xiàn)的!但是一張費用開銷表是獨立的。那些字符串和數(shù)字不會做任何事情。它們不會背著你私下改變,不管你用多少種不同的方式在第一列寫入了一個6
,它永遠只會是一個6
。
這就是值類型的偉大之處。
Objective-C 和 C 具有值類型,但是 Swift 允許你在以前不能使用的場景下使用它們。比如,泛型系統(tǒng)的抽象特性可以讓泛型類型在值和引用類型間互換。數(shù)組既可以存儲 Int
也能存儲 UIView
。Swift 中的枚舉的表現(xiàn)更是大放異彩,因為它們現(xiàn)在可以攜帶某些值和方法了。結構體可以遵守協(xié)議和指定的方法。
Swift 增強了對值類型的支持,這提供了一個巨大的機會:值類型成為了使代碼簡單的一個非常靈活的工具。你可以使用它們將孤立的、可預見組件從臃腫的類中抽離出來。默認情況下,值類型被強制使用或者至少說被鼓勵使用在屬性上,來使得工作更清晰。
在這部分,我會描述一些鼓勵使用值類型特性的情形。值得注意的是,你也可以讓對象包含這些特性,但是語言本身決定了你沒必要去那么做。如果你在代碼里看到了一個對象,你不會期待它出現(xiàn)這些特性;然而,如果你看到了一個值類型,那么對這些特性的期望就是合理的。誠然,不是所有的值類型都有這些屬性 —— 我們稍后會討論這個 —— 但是這是合理的概括。
總的來說,值類型不具有行為。它是非常穩(wěn)定的。它保存數(shù)據(jù)并暴露使用這些數(shù)據(jù)進行計算的方法。其中的一些方法可能會使值類型本身發(fā)生改變,但是控制流卻還是嚴格地受控于該實例的唯一所有者。
這太好了!這下更容易思考被唯一所有者直接調用才會執(zhí)行的代碼了。
相比之下,一個對象可能將它自己注冊為一個定時器的 target。它可能會接收到來自系統(tǒng)的事件。這樣的交互意味著引用類型需要有多個擁有者。因為值類型只能有一個所有者并且沒有析構函數(shù),所以我們也不容易寫出會對自己產(chǎn)生副作用影響的值類型。
一個典型的值類型對任何外部組件的行為都沒有隱式的依賴。一眼看上去,與引用類型和其未知個數(shù)的所有者之間的交互相比,值類型和它的唯一所有者之間的交互要簡單多了。它是孤立的。
如果你正在獲取一個可變實例的引用,那么你對該實例的所有其他所有者都產(chǎn)生了隱式依賴:它們可能在任何時刻背著你偷偷改變它。
因為每次將值類型賦給一個新變量的時候,該值類型都是被復制的,所以,所有的這些副本都是可交換的。
你可以安全地存儲傳遞給你的值,然后在將來就像使用新
值一樣使用它們。人們區(qū)分該實例和其他實例的唯一依據(jù)就是實例所包含的數(shù)據(jù)。可交換還意味著不管一個給定的值是如何進行構造的,我們通過 == 進行比較的話,同樣的值在任何情形下都是相等的。
所以如果你使用值類型同系統(tǒng)里的組件進行通信,你可以很容易地改變你的組件圖。你有沒有一個視圖用來描繪觸摸采樣的序列?你不用觸及視圖代碼,只通過一個觸摸采樣序列的組件,就可以補償觸摸延遲,依據(jù)前一次的采樣,追加用戶手指將要移動位置的預測,然后返回一個新的序列。你可以自信地將另一個新的組件的輸出傳給視圖 —— 因為視圖分辨不出區(qū)別。
為值類型編寫單元測試不需要花哨的模擬 (mocking) 框架。你可以直接從應用程序中的活的
實例中構造出無分別的值。上面提到的觸摸預測組件很容易進行單元測試:可預測的值類型輸入,可預測的值類型輸出等,它們都不會產(chǎn)生副作用。
這是巨大的優(yōu)勢。在以對象行為主導的傳統(tǒng)架構中,你必須要測試與正被測試的對象的交互以及與系統(tǒng)的其他部分之間的交互。那通常意味著笨拙的模擬,或者為了建立那樣的關系而添加了大量的設置代碼。值類型是孤立的,穩(wěn)定的和可交換的,所以你可以直接地構建一個值,調用一個方法,然后檢查輸出。更簡單的測試,更大的覆蓋范圍意味著代碼更容易修改。
雖然值類型的結構鼓勵這些特性,但是你也可以使值類型違反這些特性。
包含不是由所有者調用而執(zhí)行的代碼的值類型,通常是不可預測的,并且通常情況下應該是要避免使用的。比如:一個結構體的構造函數(shù)可能調用 dispatch_after
來安排一些工作。但是將該結構體的一個實例傳遞給函數(shù)的時候,因為進行了一次復制,就會不經(jīng)意地重復做這件事情。值類型應該是穩(wěn)定的。
包含引用的值類型通常都不是孤立的,并且應該避免使用它們:它們攜帶了對那個對象的所有其他所有者的依賴。這些值類型也不是易交換的,因為外部引用可能以復雜的方式與系統(tǒng)的其他部分相聯(lián)系。
我當然不是建議使用穩(wěn)定的值類型來構建所有的事情。
更精確地講,對象也是有用的,因為它們不包含我上面所說的屬性。一個對象在系統(tǒng)中扮演著實體的角色。它有身份,具有行為,通常也是獨立的。
那種行為通常復雜并且不容易思考,但是其中一些細節(jié)通常可以由簡單的值和孤立地函數(shù)調用表現(xiàn)出來。那些細節(jié)不會和對象的復雜的行為交織在一起。通過將它們分離,對象的行為就會變得清晰。
可以將對象看成是一個薄的、命令式的層,它位于可預測的、純值類型的層之上。
對象維護通過值來定義的狀態(tài),但是那些值其實是獨立于對象被設定和操作的。值層 (value layer) 實際上沒有狀態(tài);它僅僅用來表示和變換數(shù)據(jù)。那些數(shù)據(jù)作為狀態(tài)可能有 (也可能沒有) 高層的意味,這取決于使用值的上下文。
對象就像 I/O 和網(wǎng)絡一樣會有副作用,但是數(shù)據(jù)、計算和重要的決策最后都驅使這些副作用存在于值類型層。對象就像薄膜,通過這一層薄膜,將那些純凈的、可預測的結果引入副作用的不純凈的領域。
對象可以和其他對象通信,但是通常它們發(fā)送的是值,而不是引用,除非它們確實想要和外部不可或缺的層創(chuàng)建一個持久的連接。
值類型能夠使你構建非常清晰,簡單,更容易測試的典型架構。
值類型與外部狀態(tài)通常沒有依賴或者只有很少的依賴,所以當你思考它們的時候,你只需要考慮很少的一部分。
值類型是內在可組合的和可重用的,因為它們是可交換的。
最后,一個值類型層允許你從應用程序穩(wěn)定的業(yè)務邏輯中獨立出活躍的行為元素。代碼越穩(wěn)定,你的系統(tǒng)會變得越容易測試和修改。
Boundaries, by Gary Bernhardt, proposes a similar two-level architecture and elaborates on its benefits for concurrency and testing.
Are We There Yet?, by Rich Hickey, elaborates on the distinctions between value, state, and identity.