Swift5 值類型與引用類型探究

在Swift中結構體和類最大的區別就是結構體是值類型,類是引用類型。今天我們探究一下值類型和引用類型

一、類型表

值類型表

  • 結構體
  • 枚舉
  • 元組(tuple)
  • 基本類型(Int,Double,Bool等)
  • 集合(Array, String, Dictionary, Set)

引用類型表

  • 閉包

二、存放區域

在 Swift 中, \color{red}{值類型,存放在棧區}\color{red}{引用類型,存放在堆區}。但是有些值類型,如字符串或數組,會間接地將項保存在堆中。所以它們是由引用類型支持的值類型。

三、 值類型 和 引用類型

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

3.1值類型

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

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

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

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

// 0x00007ffee5eec040
// 0x00007ffee5eec030

在 Swift 中,雙等號( ==\color{red}{==} & !=\color{red}{!=})可以用來比較變量存儲的內容是否一致,如果要讓我們的 struct\color{red}{struct} 類型支持該符號,則必須遵守Equatable\color{red}{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

3.2 引用類型

引用類型的賦值是淺拷貝(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 中,可以使用以下方法來打印引用類型變量指向的內存地址。從中即可發現,兩個變量指向的是同一塊內存空間。

print(Unmanaged.passUnretained(dogA).toOpaque())
print(Unmanaged.passUnretained(dogB).toOpaque())

// 0x0000600000d6cac0
//0x0000600000d6cac0

在 Swift 中,三等號(===\color{red}{===} & !==\color{red}{!==})可以用來比較引用類型的引用(即指向的內存地址)是否一致。也可以在遵守Equatable\color{red}{ Equatable} 協議後,使用雙等號(==\color{red}{==} &!=\color{red}{ !=})用來比較變量的內容是否一致。

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\color{red}{ResolutionStruct}結構體,以及一個 ResolutionClass\color{red}{ResolutionClass} 類。這裏爲了方便打印對象屬性,ResolutionClass\color{red}{ResolutionClass }類遵從了 CustomStringConvertible\color{red}{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
}

但是如果要改變參數值或引用,那麼就可以在函數體內部直接聲明同名變量,並把原有變量賦值於新變量,那麼這個新的變量就可以更改其值或引用。那麼在函數參數的作用域和生命週期是什麼呢?我們來測試一下,定義兩個函數,目的爲交換內部的 height\color{red}{height}width\color{red}{width }

值類型

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\color{red}{inout}是 Swift 中的關鍵字,可以放置於參數類型前,冒號之後。使用 inout\color{red}{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\color{red}{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

需要注意的是:

使用 inout\color{red}{inout} 關鍵字的函數,在調用時需要在該參數前加上 & 符號
inout\color{red}{inout} 參數在傳入時必須爲變量,不能爲常量或字面量(literal)
inout\color{red}{inout} 參數不能有默認值,不能爲可變參數
inout\color{red}{inout} 參數不等同於函數返回值,是一種使參數的作用域超出函數體的方式
多個 inout\color{red}{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\color{red}{inout} 參數的傳遞過程:

當函數被調用時,參數值被拷貝
在函數體內,被拷貝的參數修改
函數返回時,被拷貝的參數值被賦值給原有的變量
官方稱這個行爲爲:copyincopyout\color{red}{copy-in copy-out}callbyvalueresult\color{red}{call by value result}。我們可以使用 KVO 或計算屬性來跟蹤這一過程,這裏以計算屬性爲例。排除在調用函數之前與之後的 centerGETTERcall\color{red}{center-GETTER- call},從中可以發現:參數值先被獲取到(setter 被調用),接着被設值(setter 被調用)。

根據inout\color{red}{inout} 參數的傳遞過程,可以得知:inout\color{red}{inout} 參數的本質與引用類型的傳參並不是同一回事。inout\color{red}{inout}參數打破了其生命週期,是一個可變淺拷貝。在 Swift 中,也徹底摒除了在逃逸閉包(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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章