Swift 中的值類型與引用類型使用指北

Swift 中的值類型與引用類型使用指北

在本文中,咱們將探索值類型與引用類型語義的不一樣之處,在 Swift 中使用值類型的一些鮮明特徵和關鍵的好處。而後咱們會關注在設計程序時,什麼時候使用值類型或者引用類型。html

Swift 中的值類型和引用類型

Swift 是一種多範式的編程語言。它有類,這是構成面向對象編程的基石。類在 Swift 中能夠定義屬性和方法,指定構造器,符合協議,支持集成和多態。Swift 也是一種面向協議的編程語言,經過功能豐富的協議和結構體,能夠在沒有繼承的狀況下實現抽象和多態。在 Swift 中,函數是第一類型,它能夠賦給變量,做爲參數和返回值在多個函數之間傳遞。所以 Swift 也適用於函數式編程。前端

對於多數面嚮對象語言的開發者來講,Swift 中最大的不一樣就是結構體的豐富功能。除了繼承之外,你在一個類裏能夠作什麼,在結構體中一樣能夠作到。這就引起了問題 —— 什麼時候並如何使用結構體和類。更通俗的說,問題是在 Swift 中什麼時候並如何使用值類型和引用類型。android

爲了完整須要提醒一下,Swift 中的值類型並不只僅只有結構體。枚舉和元組也是值類型。一樣地,引用類型並不僅有類,函數也是引用類型。不過函數、枚舉和元組在使用時更加特定化。Swift 在值類型和引用類型的爭論中心都集中在結構體和類上。這是本文中的主要重點,因此在本文中術語值類型和引用類型能夠和術語結構體和類相互轉換。ios

如今讓咱們從一些基本原理開始,即值和引用語義的區別。git

值與引用

使用值語義,變量和分配給變量的數據在邏輯上是統一的。因爲變量存在於棧上,值類型在 Swift 中被稱爲棧分配。確切地說,全部的值類型實例並不一直在棧上。一些可能只存在於 CPU 寄存器中,另外一些可能實際在堆上分配。從邏輯上講,值類型的實例能夠被認爲是包含在被賦值的變量之中。在變量和值之間存在一對一的關係。變量所包有的值不能獨立於變量進行操做。github

另外一方面,在使用引用語義時,變量和數據是不一樣的。引用類型的實例在堆中分配,變量只包含一個對存儲數據的內存位置的引用。一個實例引用多個變量是能夠的也是很常見的。任何這些引用均可以用來操做實例。編程

這會對將值或引用類型實例分配給新變量或傳遞給函數時發生一些影響。因爲值類型實例只能擁有一個全部者,實例被複制,並將副本分配給新變量或傳入某函數。每一個副本均可以修改而互不影響。對於引用類型,只有引用被複制,而且新變量或函數得到對同一實例的新引用。若是使用任何引用修改引用類型實例,則會影響全部其餘引用持有者,由於它們持有的都是對同一實例的引用。swift

咱們來看看代碼。後端

struct CatStruct {
    var name: String
}

let a = CatStruct(name: "Whiskers")
var b = a
b.name = "Fluffy"

print(a.name)   // Whiskers
print(b.name)   // Fluffy
複製代碼

咱們定義了一個結構體表示一隻貓,有一個 name 屬性。咱們建立一個 CatStruct 實例,把它賦給一個變量,而後把這個變量賦給一個新的變量,並用新變量改變 name 屬性。因爲結構體是值語義,賦值給新變量的行爲會致使實例被複制,而後咱們獲得了兩個不一樣名字的 CatStruct安全

如今,咱們用類作一樣的事:

class CatClass {
    init(name: String) {
        self.name = name
    }

    var name: String
}

let x = CatClass(name: "Whiskers")
let y = x
y.name = "Fluffy"

print(x.name)   // Fluffy
print(y.name)   // Fluffy
複製代碼

在這種狀況下,用新變量改變 name 屬性也會修改第一個變量的 name 屬性。這是由於類是引用語義,賦值給新變量的行爲不會建立一個新的實例,兩個變量持有對同一個實例的引用,這致使隱式數據共享,這可能會對你如何並什麼時候使用引用類型產生影響。

可變性的不一樣概念

爲了理解可變性在值類型和引用類型之間的差別,咱們必需要分清楚變量可變性實例可變性

咱們上面已經知道,值類型實例和被賦值的變量在邏輯上是一致的。所以,若是變量是不可變的,那不管該實例是否有可變屬性或者可變方法,變量都會忽略讓實例不可變。只有當值類型的實例賦給一個可變變量時,實例的可變性才能夠起做用。

對於引用類型,實例和被賦值的變量是不一樣的,所以他們的可變性也是不一樣的。當咱們聲明一個不可變的變量引用一個實例,咱們能肯定的是,這個變量的引用永遠不會改變。即它總會指向同一個實例。實例的可變屬性仍是能夠經過這個或者其餘的引用改變。若是要讓類實例不可變,必須保證它的全部存儲屬性都是不可變的。

在剛纔的代碼中,咱們看到,能夠聲明 a 將第一個 CatStruct 實例做爲 let 常量,由於它不會被修改。而 b 必須被聲明爲一個 var,由於咱們修改了它的 name 屬性和值。對於 CatClassxy 都被聲明爲 let 常量,然而咱們能修改 name 屬性。

定義爲值類型的特徵

爲了能更好的理解何時以及如何使用值類型,咱們須要看一下定義爲值類型的一些特徵:

  1. **基於屬性的相等:**任何兩個同類型值,其屬性相等,均可以認爲他們是相等的。考慮一個貨幣類型,它表示貨幣具備貨幣和金額屬性。若是咱們建立一個 5 美圓的實例,它與任何其餘 5 美圓實例都相等。
  2. **淡化的標識及生明週期:**值類型沒有固定的身份。它僅由其屬性而定義。對於數字 2 或者 「Swift」 這種簡單的值就是這種狀況。對於複雜的值來講也是如此。值也沒有須要保存狀態變化的生命週期。它能夠隨時被建立、銷燬或重建。表明 5 美圓的貨幣實例,等於表明 5 美圓的任何其餘實例,不管這兩個實例是什麼時候或如何建立。
  3. **可替代性:**沒有明確的標識和生命週期給了值類型可替代性,這意味着,若是兩個實例相等,即它們經過了基於屬性的相等測試,那麼任何實例均可以被自由地替代。回到咱們的貨幣類型例子,一旦咱們建立了一個表明 5 美圓的實例,程序能夠根據狀況自由的建立或放棄這個實例的副本。不管什麼時候咱們須要遞交一個 5 美圓的實例,這個 5 美圓的實例是不是先前建立的那個已經可有可無,咱們要關心的是值的屬性。

使用值類型的優勢

1. 效率

引用類型在堆上分配,這比在棧上分配要昂貴的多。爲了確保在引用類型不須要時內存被釋放,須要保持一個對每一個引用類型的全部活動的引用計數,並在沒有引用時銷燬實例。值類型沒有這種開銷,因此在建立和複製上很高效。值類型的複製是廉價的,由於值類型的實例在不變(constant)的時間被複制。

Swift 實現了內置的可擴展的數據結構,好比 StringArrayDictionary 等等。然而,這些並不能在棧上分配,由於他們的大小在編譯時是不知道的。爲了能有效地使用堆分配而且保有值語義,Swift 使用一種名爲寫時複製的優化技術。這意味着每一個複製的實例都是邏輯意義上的副本,只有當複製的實例發生變化時纔會在堆上建立實際的副本,在此以前,全部的邏輯副本都會指向相同的底層實例。由於更少的副本被建立,而且在建立的時候,涉及了固定數量的引用計數操做,因此提供了更好的性能。若是須要,這種性能優化還能夠對自定義值類型使用。

2. 可預測的代碼

使用引用類型時,持有對實例的引用的代碼的任何部分都不能肯定該實例包含的內容,由於可使用任何其餘引用來修改該實例包含的內容。因爲值類型實例在複製時沒有隱式數據共享,因此咱們不須要考慮代碼的某部分的行爲會影響其餘部分行爲所形成的意外後果。並且,當咱們看到一個變量聲明爲 let 常量並持有一個值類型的實例時,咱們能夠確定,不管如何定義值類型,該值都不能被修改。這爲代碼的行爲提供了強有力的守護以及細粒度的控制,讓代碼變的易於推理和預測。

有人可能會爭辯說,能夠編寫代碼,使得每次將引用類型實例交給新全部者時,都會建立一個副本。 可是這會致使不少防護性複製,這樣效率會很是低,由於複製一個引用類型會帶來很大的開銷。若是正在複製的引用類型實例具備也是引用類型實例的屬性,而且咱們但願避免任何隱式數據共享,則每次都必須建立深度拷貝,這會使讓性能更糟。咱們也能夠嘗試經過使全部引用類型不可變來解決共享狀態和可變性的問題。可是這仍然會涉及到不少低效率的複製,並且沒法改變引用類型的狀態會失去引用類型的用意。

3. 線程安全

值類型實例能夠在多線程環境中使用,而不用擔憂一個線程正在改變另外一個線程實例的狀態。因爲沒有競態條件和死鎖,因此沒有必要實現同步機制。使用值類型編寫多線程的代碼變得更簡單、更安全、更高效。

4. 無內存泄漏

Swift 使用自動引用計數,並在沒有引用的狀況下,釋放引用類型實例。這解決了正常事件過程當中的內存泄漏問題。不過,經過強循環引用仍會內存泄漏,即當兩個類實例彼此強引用互相阻止彼此的釋放。當一個類與一個閉包(在 Swift 中也是引用類型)彼此強引用也會發生相同的狀況。因爲值類型沒有引用,因此內存泄漏的問題也就不存在。

5. 易於測試

由於引用類型的生命週期會保有狀態,因此在對引用類型進行單元測試時,常用模擬框架來觀察各類方法被調用時對測試對象的狀態和行爲的影響。並且因爲引用類型實例的行爲會隨狀態的變化而改變,一般須要設置代碼來保證測試對象處於正確的狀態。對值類型而言,要關心的所有是值類型的屬性。因此咱們須要作的,就是建立一個新的值,這個值的屬性和指望的值屬性相同。

用值類型和引用類型設計程序

值類型和引用類型不該該被看做是相互競爭的。他們不一樣的語義和行爲,讓他們適用於不一樣的情景。咱們的目的是理解並運用值和引用語義,讓他們以最能知足應用目標的方式結合起來。

1. 使用引用類型模擬具備標識的實體

幾乎全部現實世界領域都有在生命週期裏保持着標識和狀態的實體。這些實體應該使用類來建模。

考慮有一個使用員工類型來表明員工的薪酬應用。簡單地,假設只存儲員工的姓和名。可能有兩個或者更多的員工實例的姓名相同,可是這並不能讓他們相等,由於在現實世界中,這些實例表明着不一樣的員工。

若是把一個員工類實例賦給一個新的變量或者把它傳到一個函數裏,新的引用會指向相同的實例。這是咱們能夠肯定的。例如,若是咱們在應用的某個模塊中使用一個引用來記錄員工的工時,那麼當應用另外一個模塊計算每個月工資時,它使用的都是具備正確工時的同一個實例。一樣,若是在某個位置更新員工的地址,那麼咱們對員工的全部引用都會更新爲正確的地址,由於他們是對同一實例的引用。

若是嘗試使用結構體來模擬員工的話會致使錯誤而且先後矛盾,由於每次把員工實例賦給一個變量或者傳給一個函數時,它會被複制。程序中不一樣的部分會以它們各自的實例結束,而且其中某部分狀態改變並不會在其餘部分體現出來。

2. 用值類型來封裝狀態和暴露行爲

雖然有標識和生命週期的實體須要用類來建模,可是須要用值類型來封裝它們的狀態,表示相關的業務而且暴露行爲。

繼續以員工類型爲例。假設要保留每一個員工的我的數據,工資績效信息。咱們能夠建立我的信息工資績效值類型,將狀態、業務規則和行爲這些元素聯繫在一塊兒。這可讓類不那麼臃腫,由於它只負責維護標識,而它包含的值類型實例會處理該狀態的各類元素和相關行爲。

這也很是符合單一原則。例如,相比於員工類型不得不實現一些方法來暴露各類層面的行爲,客戶代碼只對員工的績效感興趣,因此交給績效實例來處理。由於處理的是值類型,咱們無需擔憂隱式數據共享與客戶端背後變化,而對員工實例的狀態產生影響。

這種方式也更加適用於多線程。表示引用類型實例狀態的各類元素的值類型實例副本,能夠自由地切換到不一樣線程上的進程,而不須要同步。這能夠提升性能,並提升應用交互的響應。

3. 上下文的重要性

要注意的是,有時值類型和引用類型的選擇是由上下文驅動的。應用開發不是絕對意義上的對現實世界的建模練習,而是建模問題的具體方面,以知足給定的用例。所以,要判斷在應用程序的上下文中使用值語義仍是引用語義,具體取決於實體在相關領域問題中扮演的角色。

想想前面介紹的 CatStructCatClass 類型。咱們更願意使用哪種模型來模擬寵物貓呢?因爲實例將表明一隻真正的貓,因此應該使用一個類。例如,當咱們把貓交給獸醫來打疫苗時,咱們不但願獸醫給一隻貓的副本打疫苗,若是使用一個結構體,就會發生這樣的事情。可是,若是咱們正在設計一個處理寵物貓的飲食習慣的應用,那麼就應該使用結構體來處理通常意義上的貓,而不是尋找一隻特定標識的貓。對於這樣的應用,咱們的 CatStruct 不會擁有 name 屬性,但可能有消耗食物類型,天天的服務數量等的屬性。

不久前,咱們使用貨幣類型做爲一個值爲模型的概念的絕佳例子。在銀行,金融或其餘應用的狀況下,咱們只關心貨幣的屬性,即貨幣的多少和種類。可是,若是咱們正在創建一個實物貨幣的印刷,分配和最終處理的應用,咱們就須要將每一個紙幣視爲具備惟一標識和生命週期的實體。

相同地,對於爲輪胎製造商開發的應用程序來講,每一個輪胎均可能是一個具備惟一標識和生命週期的實體,用於銷售點以追蹤退貨,保修索賠等。可是,對製造汽車的公司而言,他們也許不想看輪胎的屬性來跟蹤哪輛車使用哪一個輪胎,儘管他們能夠看到他們製造的汽車具備獨特的標識和生命週期。

4. 基於屬性相等的測試

值類型沒有固定的標識來區分它是不是那個類型實例。惟一比較它們的方式就是比較它們的屬性。事實上,基於屬性相等性的概念在值類型中是很是基本的,因此決定一個特定的類型是值類型仍是引用類型,它能夠做爲一個指引。若是一個類型的兩個實例不能僅使用基於屬性的相等來比較的話,那咱們就要處理一些元素的標識,這一般意味着他們是引用類型,或者它們能夠用值和引用語義區分。

實際上,這意味着要比較任何兩個實例是否相等都要使用 == 運算符。所以,全部的值類型都必須符合 Equatable 協議。

5. 結合值類型和引用類型

如上面提到過的,把引用類型的屬性封裝爲值類型的實例,以達到封裝狀態,表示業務規則而且暴露行爲的目的是很是可取的。這些值類型能夠高效傳遞,而不用擔憂意外後果,如線程安全性等。可是,值類型應該保存引用類型的實例嗎?這一般應該避免,由於在值類型上使用引用類型屬性會引入堆分配,引用計數和隱式數據共享,影響值類型的性能和其餘優勢。事實上,它會致使值類型失去其基於屬性的平等,淡化標識和可替代性的特色。所以,重要的是要遵照規則,不能以損害二者完整性的方式來結合值與引用語義。

有不少方式描述了值類型和引用類型是如何在實際應用中工做的。如 Andy Matuschak這篇文章中所說的:把對象看做是可預測的純淨的值層之上的一個輕薄的必要的層。在 Andy 的文章的參考文獻部分是 Gary Bernhardt此次演講,一種使用他稱之爲的函數性核心和命令式外殼來構建系統的方法。函數核心由純粹的值,特定領域邏輯和業務規則組成。很容易得出,這套系統有利於併發而且易於測試,由於它經過命令式外殼與外部依賴隔離,所以保留了狀態並鏈接到用戶界面,持久化機制,網絡等等。

Swift 標準庫與 Cocoa 框架

Swift 的標準庫主要由值類型組成。全部的內建基本類型和集合都是用結構體實現的。構成 Cocoa 框架的部分主要由類構成。有些地方須要類的緣由是,類對於 MVC,用戶界面元素,網絡鏈接,文件處理等等是很恰當的方式。

可是 Cocoa 在 Foundation 框架裏也有不少類是值類型的,不過做爲引用類型而存在,由於他們是用 Objective-C 來編寫的。這就是 Swift 標準覆蓋的地方,爲愈來愈多的 Objective-C 引用類型提供了值類型的橋接。更多橋接類型和 Swift 與 Cocoa 框架之間交互的細節,能夠看看蘋果開發者網站上的這一頁

結論

Swift 提供了強大而高效的值類型,讓咱們的代碼更加高效,可預測並且線程安全。這就須要理解值和引用語義之間的差別,才能以最能知足應用程序目標的方式來結合值類型和引用類型。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索