swift的值類型和引用類型

前言

最近在學設計模式中,發現 Swift 中的 struct,class 以及 enum 在通常的使用中可以作到互相替換,所以探究其背後的邏輯就十分有必要。而這一問題又引出了 Swift 中的值類型和引用類型的區別。在網上搜尋一番,雖然也找到不少很棒的資料,不過有的有些過期,或是比較分散,所以總結一篇,以便本身加深印象,也方便與你們交流。html

因爲 Swift 中的 struct 爲值類型,class 爲引用類型,所以文中以這兩種類型爲表明來具體闡述。swift

stack & heap

內存(RAM)中有兩個區域,棧區(stack)和堆區(heap)。在 Swift 中,值類型,存放在棧區;引用類型,存放在堆區。設計模式

class RectClass { var height = 0.0 var width = 0.0 } struct RectStruct { var height = 0.0 var width = 0.0 } var rectCls = RectClass() var rectStrct = RectStruct() 
 
stack & heap in RAM

值類型 & 引用類型

值類型(Value Type)

值類型,即每一個實例保持一份數據拷貝。多線程

在 Swift 中,典型的有 struct,enum,以及 tuple 都是值類型。而平時使用的 IntDoubleFloatStringArrayDictionarySet 其實都是用結構體實現的,也是值類型。閉包

Swift 中,值類型的賦值爲深拷貝(Deep Copy),值語義(Value Semantics)即新對象和源對象是獨立的,當改變新對象的屬性,源對象不會受到影響,反之同理。app

 
值類型
struct CoordinateStruct { var x: Double var y: Double } var coordA = CoordinateStruct(x: 0, y: 0) var coordB = coordA coordA.x = 100.0 print("coordA.x -> \(coordA.x)") print("coordB.x -> \(coordB.x)") // coordA.x -> 100.0 // coordB.x -> 0.0 

若是聲明一個值類型的常量,那麼就意味着該常量是不可變的(不管內部數據爲 var/let)。ide

let coordC = CoordinateStruct(x: 0, y: 0) // WRONG: coordC.x = 100.0 

在 Swift 3.0 中,可使用 withUnsafePointer(to:_:) 函數來打印值類型變量的內存地址,這樣就能看出兩個變量的內存地址並不相同。函數

withUnsafePointer(to: &coordA) { print("\($0)") } withUnsafePointer(to: &coordB) { print("\($0)") } // 0x000000011df6ec10 // 0x000000011df6ec20 

在 Swift 中,雙等號(== & !=)能夠用來比較變量存儲的內容是否一致,若是要讓咱們的 struct 類型支持該符號,則必須遵照 Equatable 協議。學習

extension CoordinateStruct: Equatable { static func ==(left: CoordinateStruct, right: CoordinateStruct) -> Bool { return (left.x == right.x && left.y == right.y) } } if coordA != coordB { print("coordA != coordB") } // coordA != coordB 

引用類型(Reference Type)

引用類型,即全部實例共享一份數據拷貝。測試

在 Swift 中,class 和閉包是引用類型。引用類型的賦值是淺拷貝(Shallow Copy),引用語義(Reference Semantics)即新對象和源對象的變量名不一樣,但其引用(指向的內存空間)是同樣的,所以當使用新對象操做其內部數據時,源對象的內部數據也會受到影響。

 
引用類型
class Dog { var height = 0.0 var weight = 0.0 } var dogA = Dog() var dogB = dogA dogA.height = 50.0 print("dogA.height -> \(dogA.height)") print("dogB.height -> \(dogB.height)") // dogA.height -> 50.0 // dogB.height -> 50.0 

若是聲明一個引用類型的常量,那麼就意味着該常量的引用不能改變(即不能被同類型變量賦值),但指向的內存中所存儲的變量是能夠改變的。

let dogC = Dog() dogC.height = 50 // WRONG: dogC = dogA 

在 Swift 3.0 中,可使用如下方法來打印引用類型變量指向的內存地址。從中便可發現,兩個變量指向的是同一塊內存空間。

print(Unmanaged.passUnretained(dogA).toOpaque()) print(Unmanaged.passUnretained(dogB).toOpaque()) // 0x0000600000031380 // 0x0000600000031380 

在 Swift 中,三等號(=== & !==)能夠用來比較引用類型的引用(即指向的內存地址)是否一致。也能夠在遵照 Equatable 協議後,使用雙等號(== & !=)用來比較變量的內容是否一致。

if (dogA === dogB) { print("dogA === dogB") } // dogA === dogB if dogC !== dogA { print("dogC !== dogA") } // dogC !== dogA extension Animal: Equatable { static func ==(left: Animal, right: Animal) -> Bool { return (left.height == right.height && left.weight == right.weight) } } if dogC == dogA { print("dogC == dogA") } // dogC == dogA 

參數 與 inout

預備

定義一個 ResolutionStruct 結構體,以及一個 ResolutionClass 類。這裏爲了方便打印對象屬性,ResolutionClass 類聽從了 CustomStringConvertible 協議。

struct ResolutionStruct { var height = 0.0 var width = 0.0 } class ResolutionClass: CustomStringConvertible { var height = 0.0 var width = 0.0 var description: String { return "ResolutionClass(height: \(height), width: \(width))" } } 

函數傳參

在 Swift 中,函數的參數默認爲常量,即在函數體內只能訪問參數,而不能修改參數值。具體來講:

  1. 值類型做爲參數傳入時,函數體內部不能修改其值
  2. 引用類型做爲參數傳入時,函數體內部不能修改其指向的內存地址,可是能夠修改其內部的變量值
func test(sct: ResolutionStruct) { // WRONG: sct.height = 1080 var sct = sct sct.height = 1080 } func test(clss: ResolutionClass) { // WRONG: clss = ResolutionClass() clss.height = 1080 var clss = clss clss = ResolutionClass() clss.height = 1440 } 

可是若是要改變參數值或引用,那麼就能夠在函數體內部直接聲明同名變量,並把原有變量賦值於新變量,那麼這個新的變量就能夠更改其值或引用。那麼在函數參數的做用域和生命週期是什麼呢?咱們來測試一下,定義兩個函數,目的爲交換內部的 heightwidth

值類型

func swap(resSct: ResolutionStruct) -> ResolutionStruct { var resSct = resSct withUnsafePointer(to: &resSct) { print("During calling: \($0)") } let temp = resSct.height resSct.height = resSct.width resSct.width = temp return resSct } var iPhone4ResoStruct = ResolutionStruct(height: 960, width: 640) print(iPhone4ResoStruct) withUnsafePointer(to: &iPhone4ResoStruct) { print("Before calling: \($0)") } print(swap(resSct: iPhone4ResoStruct)) print(iPhone4ResoStruct) withUnsafePointer(to: &iPhone4ResoStruct) { print("After calling: \($0)") } // ResolutionStruct(height: 960.0, width: 640.0) // Before calling: 0x00000001138d6f50 // During calling: 0x00007fff5a512148 // ResolutionStruct(height: 640.0, width: 960.0) // ResolutionStruct(height: 960.0, width: 640.0) // After calling: 0x00000001138d6f50 

小結:在調用函數先後,外界變量值並沒有由於函數內對參數的修改而發生變化,並且函數體內參數的內存地址與外界不一樣。所以:當值類型的變量做爲參數被傳入函數時,至關於建立了新的常量並初始化爲傳入的變量值,該參數的做用域及生命週期僅存在於函數體內。

func swap(resCls: ResolutionClass) { print("During calling: \(Unmanaged.passUnretained(resCls).toOpaque())") let temp = resCls.height resCls.height = resCls.width resCls.width = temp } let iPhone5ResoClss = ResolutionClass() iPhone5ResoClss.height = 1136 iPhone5ResoClss.width = 640 print(iPhone5ResoClss) print("Before calling: \(Unmanaged.passUnretained(iPhone5ResoClss).toOpaque())") swap(resCls: iPhone5ResoClss) print(iPhone5ResoClss) print("After calling: \(Unmanaged.passUnretained(iPhone5ResoClss).toOpaque())") // ResolutionClass(height: 1136.0, width: 640.0) // Before calling: 0x00006000000220e0 // During calling: 0x00006000000220e0 // ResolutionClass(height: 640.0, width: 1136.0) // After calling: 0x00006000000220e0 

小結:在調用函數先後,外界變量值隨函數內對參數的修改而發生變化,並且函數體內參數的內存地址與外界一致。所以:當引用類型的變量做爲參數被傳入函數時,至關於建立了新的常量並初始化爲傳入的變量引用,當函數體內操做參數指向的數據,函數體外也受到了影響。

inout

inout 是 Swift 中的關鍵字,能夠放置於參數類型前,冒號以後。使用 inout 以後,函數體內部能夠直接更改參數值,並且改變會保留。

func swap(resSct: inout ResolutionStruct) { withUnsafePointer(to: &resSct) { print("During calling: \($0)") } let temp = resSct.height resSct.height = resSct.width resSct.width = temp } var iPhone6ResoStruct = ResolutionStruct(height: 1334, width: 750) print(iPhone6ResoStruct) withUnsafePointer(to: &iPhone6ResoStruct) { print("Before calling: \($0)") } swap(resSct: &iPhone6ResoStruct) print(iPhone6ResoStruct) withUnsafePointer(to: &iPhone6ResoStruct) { print("After calling: \($0)") } // ResolutionStruct(height: 1334.0, width: 750.0) // Before calling: 0x000000011ce62f50 // During calling: 0x000000011ce62f50 // ResolutionStruct(height: 750.0, width: 1334.0) // After calling: 0x000000011ce62f50 

小結:值類型變量做爲參數傳入函數,外界和函數參數的內存地址一致,函數內對參數的更改獲得了保留。

引用類型也可使用 inout 參數,但意義不大。

func swap(clss: inout ResolutionClass) { print("During calling: \(Unmanaged.passUnretained(clss).toOpaque())") let temp = clss.height clss.height = clss.width clss.width = temp } var iPhone7PlusResClss = ResolutionClass() iPhone7PlusResClss.height = 1080 iPhone7PlusResClss.width = 1920 print(iPhone7PlusResClss) print("Before calling: \(Unmanaged.passUnretained(iPhone7PlusResClss).toOpaque())") swap(clss: &iPhone7PlusResClss) print(iPhone7PlusResClss) print("After calling: \(Unmanaged.passUnretained(iPhone7PlusResClss).toOpaque())") // ResolutionClass(height: 1080.0, width: 1920.0) // Before calling: 0x000060000003e580 // During calling: 0x000060000003e580 // ResolutionClass(height: 1920.0, width: 1080.0) // After calling: 0x000060000003e580 

須要注意的是:

  1. 使用 inout 關鍵字的函數,在調用時須要在該參數前加上 & 符號
  2. inout 參數在傳入時必須爲變量,不能爲常量或字面量(literal)
  3. inout 參數不能有默認值,不能爲可變參數
  4. inout 參數不等同於函數返回值,是一種使參數的做用域超出函數體的方式
  5. 多個 inout 參數不能同時傳入同一個變量,由於拷入拷出的順序不定,那麼最終值也不能肯定
struct Point { var x = 0.0 var y = 0.0 } struct Rectangle { var width = 0.0 var height = 0.0 var origin = Point() var center: Point { get { print("center GETTER call") return Point(x: origin.x + width / 2, y: origin.y + height / 2) } set { print("center SETTER call") origin.x = newValue.x - width / 2 origin.y = newValue.y - height / 2 } } func reset(center: inout Point) { center.x = 0.0 center.y = 0.0 } } var rect = Rectangle(width: 100, height: 100, origin: Point(x: -100, y: -100)) print(rect.center) rect.reset(center: &rect.center) print(rect.center) // center GETTER call // Point(x: -50.0, y: -50.0) // center GETTER call // center SETTER call // center GETTER call // Point(x: 0.0, y: 0.0) 

inout 參數的傳遞過程:

  1. 當函數被調用時,參數值被拷貝
  2. 在函數體內,被拷貝的參數修改
  3. 函數返回時,被拷貝的參數值被賦值給原有的變量

官方稱這個行爲爲:copy-in copy-outcall by value result。咱們可使用 KVO 或計算屬性來跟蹤這一過程,這裏以計算屬性爲例。排除在調用函數以前與以後的 center GETTER call,從中能夠發現:參數值先被獲取到(setter 被調用),接着被設值(setter 被調用)。

根據 inout 參數的傳遞過程,能夠得知:inout 參數的本質與引用類型的傳參並非同一回事。inout 參數打破了其生命週期,是一個可變淺拷貝。在 Swift 3.0 中,也完全摒除了在逃逸閉包(Escape Closure)中被捕獲。蘋果官方也有以下的說明:

As an optimization, when the argument is a value stored at a physical address in memory, the same memory location is used both inside and outside the function body. The optimized behavior is known as call by reference; it satisfies all of the requirements of the copy-in copy-out model while removing the overhead of copying. Write your code using the model given by copy-in copy-out, without depending on the call-by-reference optimization, so that it behaves correctly with or without the optimization.

做爲一種優化,當參數是一個存儲於內存中實際地址的值時,函數體內外共用相同的一塊內存地址。該優化行爲被稱做經過引用調用;其知足 copy-in copy-out 模型的全部必需條件,同時消除了拷貝時的開銷。不依賴於經過引用調用的優化,使用 copy-in copy-out 提供的模型來寫代碼,以便在進不進行優化時(都能)正確運行。

嵌套類型

在實際使用中,其實值類型和引用類型並非孤立的,有時值類型裏會存在引用類型的變量,反之亦然。這裏簡要介紹這四種嵌套類型。

值類型嵌套值類型

值類型嵌套值類型時,賦值時建立了新的變量,二者是獨立的,嵌套的值類型變量也會建立新的變量,這二者也是獨立的。

 
值類型嵌套值類型
struct Circle { var radius: Double } var circleA = Circle(radius: 5.0) var circleB = circleA circleA.radius = 10 print(circleA) print(circleB) withUnsafePointer(to: &circleA) { print("circleA: \($0)") } withUnsafePointer(to: &circleB) { print("circleB: \($0)") } withUnsafePointer(to: &circleA.radius) { print("circleA.radius: \($0)") } withUnsafePointer(to: &circleB.radius) { print("circleB.radius: \($0)") } // Circle(radius: 10.0) // Circle(radius: 5.0) // circleA: 0x000000011dc6dc90 // circleB: 0x000000011dc6dc98 // circleA.radius: 0x000000011dc6dc90 // circleB.radius: 0x000000011dc6dc98 

值類型嵌套引用類型

值類型嵌套引用類型時,賦值時建立了新的變量,二者是獨立的,但嵌套的引用類型指向的是同一塊內存空間,當改變值類型內部嵌套的引用類型變量值時(除了從新初始化),其餘對象的該屬性也會隨之改變。

 
值類型嵌套引用類型
class PointClass: CustomStringConvertible { var x: Double var y: Double var description: String { return "(\(x), \(y))" } init(x: Double, y: Double) { self.x = x self.y = y } } struct Circle { var center: PointClass } var circleA = Circle(center: PointClass(x: 0.0, y: 0.0)) var circleB = circleA circleA.center.x = 10.0 print(circleA) print(circleB) withUnsafePointer(to: &circleA) { print("circleA: \($0)") } withUnsafePointer(to: &circleB) { print("circleB: \($0)") } print("circleA.center: \(Unmanaged.passUnretained(circleA.center).toOpaque())") print("circleB.center: \(Unmanaged.passUnretained(circleB.center).toOpaque())") // Circle(center: (10.0, 0.0)) // Circle(center: (10.0, 0.0)) // circleA: 0x0000000118251fa0 // circleB: 0x0000000118251fa8 // circleA.center: 0x000060000003e100 // circleB.center: 0x000060000003e100 

引用類型嵌套值類型

引用類型嵌套值類型時,賦值時建立了新的變量,可是新變量和源變量指向同一塊內存,所以改變源變量的內部值,會影響到其餘變量的值。

 
引用類型嵌套值類型
class Circle: CustomStringConvertible { var radius: Double var description: String { return "Radius:\(radius)" } init(radius: Double) { self.radius = radius } } var circleA = Circle(radius: 0.0) var circleB = circleA circleA.radius = 5.0 print(circleA) print(circleB) print("circleA: \(Unmanaged.passUnretained(circleA).toOpaque())") print("circleB: \(Unmanaged.passUnretained(circleB).toOpaque())") withUnsafePointer(to: &circleA.radius) { print("circleA.radius: \($0)") } withUnsafePointer(to: &circleB.radius) { print("circleB.radius: \($0)") } // Radius:5.0 // Radius:5.0 // circleA: 0x000060000003bc80 // circleB: 0x000060000003bc80 // circleA.radius: 0x000060000003bc90 // circleB.radius: 0x000060000003bc90 

引用類型嵌套引用類型

引用類型嵌套引用類型時,賦值時建立了新的變量,可是新變量和源變量指向同一塊內存,內部引用類型變量也指向同一塊內存地址,改變引用類型嵌套的引用類型的值,也會影響到其餘變量的值。

 
引用類型嵌套引用類型
class PointClass: CustomStringConvertible { var x: Double var y: Double init(x: Double, y: Double) { self.x = x self.y = y } var description: String { return "(\(x), \(y))" } } class Circle: CustomStringConvertible { var center: PointClass var description: String { return "Center:\(center)" } init(center: PointClass) { self.center = center } } var circleA = Circle(center: PointClass(x: 0.0, y: 0.0)) let circleB = circleA circleA.center.x = 5.0 print(circleA) print(circleB) print("circleA: \(Unmanaged.passUnretained(circleA).toOpaque())") print("circleB: \(Unmanaged.passUnretained(circleB).toOpaque())") print("circleA.center: \(Unmanaged.passUnretained(circleA.center).toOpaque())") print("circleB.center: \(Unmanaged.passUnretained(circleB.center).toOpaque())") // Center:(5.0, 0.0) // Center:(5.0, 0.0) // circleA: 0x0000608000025fa0 // circleB: 0x0000608000025fa0 // circleA.center: 0x0000608000025820 // circleB.center: 0x0000608000025820 

總結

這篇文章是我在着手寫 Swift 中的 struct & class & enum 一文時抽離出來的一篇。主要仍是圍繞了值類型中的 struct 和引用類型中的 class,在本文 stack & heap 一節中,只是簡單描述,由於一直對此部份內容感到迷惑,也查閱不少資料,但願最近能夠總結出來一篇小文,與你們分享。

When|值類型 Value Type|引用類型 Reference Type
-----|-----|-----|-----
1|== 有意義時|=== 有意義時
2|獨立|共享,可變
3|在多線程使用的數據|-

在本文的敘述中,可能有許多說法與您平時所用的術語略有差池,例如變量指向的內存空間,其實也等價於變量指向的內存地址。在行文過程當中,查閱了不少國外的資料,也盡力將語言規範,以避免產生歧義,若是有任何錯誤或建議,您均可以在評論中直接提出,我會研究學習,虛心接受,並做出相應整改。

參考資料

WWDC 2015 Building Better Apps with Value Types in Swift
Value and Reference Types
In-Out Parameters
In-Out Parameters
Reference vs Value Types in Swift: Part 1/2

做者:萌面大道 連接:https://www.jianshu.com/p/ba12b64f6350 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索