Swift值類型&引用類型
前言
值類型和引用類型是Swift
中兩種數據存儲方式,簡單來說值類型就是直接存儲的值,引用類型就是存儲的指針,在談值類型和引用類型前可能你需要了解一些關于內存和Mach-O
的知識。下面放上我以前寫過的幾篇文章,僅供參考。
iOS內存五大區
iOS 中的虛擬內存和物理內存
Mach-O探索
簡單來說值類型可以理解為存儲在棧區或者全局區,引用類型一般存儲在堆區,下面我們來看個簡單的例子。
我們可以看到a
和t
的地址都是在棧區,因為棧區通常都是0x7
開頭。但是a
中存儲的直接就是18
這個值,t
中存儲的是個全局區的指針。這就是最簡單的值類型和引用類型的區別。
1. 值類型
值類型,即每個實例保持一份數據拷貝。
在 Swift
中,struct
,enum
,以及 tuple
都是值類型。而平時使用的 Int
、Double
、Float
、String
、Array
、Dictionary
、Set
其實都是用結構體實現的,也是值類型。
Swift
中,值類型的賦值為深拷貝(Deep Copy
),值語義(Value Semantics
)即新對象和源對象是獨立的,當改變新對象的屬性,源對象不會受到影響,反之同理。
雖然說Int
、Double
、Float
、String
、Array
、Dictionary
、Set
時使用結構體實現的,所以也是值類型,但是就我個人理解來說,這些作為值類型好像就是那么理所當然的,當然對于很長的String
還是會通過存儲指向堆區的指針來實現,當然也會通過TaggedPointer
等技術進行優化,這里大體還是和OC
相同的,感興趣的可以看看我的另一篇文章iOS Objective-C 內存管理。說了這么多,其實我們糾結的一個問題就是struct
為什么是值類型,下面我們就來探索一番。
1.1 struct 為什么是值類型
1.1.1 結構體和類的區別
從代碼看區別
class CTeacher {
var age: Int?
var name: String!
var height: Float = 185.3
}
struct STeacher {
var age: Int
}
let ct = CTeacher()
ct.name = "testC"
let st1 = STeacher(age: 20)
let st2 = STeacher(age: 21, name: "testS", height: 180.1)
通過以上的代碼我們可以知道:
- 類中的屬性需要使用
?
、!
或者賦初始值才不會導致編譯報錯 - 結構體中的屬性不需要賦初始值,也不用使用
?
、!
- 結構體的初始化需要同時初始化結果圖內部的屬性
- 類的初始化可以不用初始化類中的屬性
- 結構體中的
optional
屬性,或者賦值的屬性可以不在結構體初始化的時候初始化
從sil代碼看區別
class CTeacher {
@_hasStorage @_hasInitialValue var age: Int? { get set }
@_hasStorage @_hasInitialValue var name: String! { get set }
@_hasStorage @_hasInitialValue var height: Float { get set }
@objc deinit
init()
}
struct STeacher {
@_hasStorage var age: Int { get set }
@_hasStorage @_hasInitialValue var name: String? { get set }
@_hasStorage @_hasInitialValue var height: Float { get set }
init(age: Int, name: String? = nil, height: Float = 185.3)
}
通過sil
代碼我們可以看到:
- 類中如果不實現自定義
init
方法就會有個init()
方法 - 結構體中會提供默認的初始化方法
1.1.2 驗證結構體是值類型
定義一個結構體:
struct Teacher {
var age: Int
var age1: Int
}
var t = Teacher(age: 18, age1: 20)
使用lldb調試:
此時我們可以看到,結構體內部直接存儲的就是結構體中的屬性的值。所以說結構體是值類型是沒問題的。
1.1.3 驗證結構體是值拷貝
此時我們創建個新的實例變量t1
,并將t
賦值給t1
,代碼如下:
struct Teacher {
var age: Int
var age1: Int
}
var t = Teacher(age: 18, age1: 20)
var t1 = t
t1.age = 22
print("end")
在修改t1
的值后我們發現t
中的數據并沒有改變,所以說t
和t1
之間是值傳遞,即t
和t1
是存儲在不同內存空間的,在var t1 = t
時,是將t
中的值,拷貝到t1
中,t1
修改時,只會修改自己內存中的數據,是不會影響到t
的內存的。
另外在打印兩個實例變量地址的時候也明顯不是一樣的。
1.1.4 通過sil驗證struct是值類型
我們查看Teacher
的init
方法:
// Teacher.init(age:age1:)
sil hidden @main.Teacher.init(age: Swift.Int, age1: Swift.Int) -> main.Teacher : $@convention(method) (Int, Int, @thin Teacher.Type) -> Teacher {
// %0 "$implicit_value" // user: %3
// %1 "$implicit_value" // user: %3
// %2 "$metatype"
bb0(%0 : $Int, %1 : $Int, %2 : $@thin Teacher.Type):
%3 = struct $Teacher (%0 : $Int, %1 : $Int) // user: %4
return %3 : $Teacher // id: %4
} // end sil function 'main.Teacher.init(age: Swift.Int, age1: Swift.Int) -> main.Teacher'
我們可以看到init
方法中并沒有調用malloc
相關的開辟內存的方法,這里也是只是將傳入的兩個值賦給初始化的結構體而已。
1.1.5 常量值類型
如果聲明一個值類型的常量,那么就意味著該常量是不可變的(無論內部數據為 var
還是let
)。
1.1.6 小結
至此我們就驗證了結構體是值類型:
- 結構體不像類一樣需要調用
malloc
等方法去開辟內存空間 - 結構體的內存中直接存儲值
- 值類型的賦值是一個值傳遞的過程,相當于深拷貝
1.2 其他
關于enum
和tuple
這里就不一一分析了,在后續的篇章中會陸續提到。
2. 引用類型
引用類型,即所有實例共享一份數據拷貝。
在 Swift
中,class
和closure
是引用類型。引用類型的賦值是淺拷貝(Shallow Copy
),引用語義(Reference Semantics
)即新對象和源對象的變量名不同,但其引用(指向的內存空間)是一樣的,因此當使用新對象操作其內部數據時,源對象的內部數據也會受到影響。
2.1 驗證類是引用類型
定義一個類
class Teacher {
var age: Int = 28
var age1: Int = 20
}
var t = Teacher()
print("end")
lldb調試
從lldb調試中我們可以看到,類實例對象指針內部存儲的是一個指向全局區的指針,而這塊內存區域才是存儲的真正的實例變量的信息,所以說類是個引用類型。
2.2 驗證類對象是指針拷貝
我們使用如下代碼進行驗證:
class Teacher {
var age: Int = 28
var name: String = "teacher1"
}
var t = Teacher()
print(t.age)
var t1 = t
t1.age = 18
print(t.age)
print("end")
通過打印結果我們可以知道,雖然我們修改的是t1
這個實例對象中age
的值,但是當我們打印t
這個實例變量的age
的值的時候也隨之改變了,所以我們就能夠確定類對象之間是指針拷貝,并且在內存地址的打印中我們也可以清晰的看見,它們指向同一片內存空間,一個改變則全部都改變。
2.4 通過sil進一步驗證類的引用類型
其實到這里也就沒什么好說的的了,在類的初始化的時候肯定是會調用alloc
方法來開辟內存空間的,這里借著上面的sil
代碼,我們來看看Info
這個類的Info.__allocating_init()
方法吧:
這里首先就調用了alloc_ref
為Info
初始化一塊內存空間。
2.5 常量引用類型
如果聲明一個引用類型的常量,那么就意味著該常量的引用不能改變(即不能被同類型變量賦值),但指向的內存中所存儲的變量是可以改變的,示例如下:
此處是不會報編譯錯誤的,這點與值類型也是不同的。
2.6 小結
至此我們就驗證了類是引用類型:
- 類需要調用
alloc
等方法去開辟內存空間 - 類的實例對象中存儲的是指針地址,這個地址中存儲的才是值
- 類的實例對象的賦值是一個指針拷貝的過程,相當于淺拷貝
3. 嵌套類型
所謂嵌套類型就是引用類型中有值類型,或者值類型中有引用類型,其實在上面的例子中已經涉及到了,下面我們通過兩兩組合,分四種情況來簡單介紹一下。
3.1 值類型嵌套引用類型
這里是在結構體中添加一個引用類型的屬性,示例代碼如下:
class Info {
var height: Int = 185
var weight: Double = 60.5
}
struct Teacher {
var age: Int = 18
var name: String = "teacher1"
var info: Info = Info()
}
var t = Teacher()
print(t.info.weight)
var t1 = t
t1.info.weight = 80
print(t.info.weight)
print(t1.info.weight)
print("end")
我們可以看到,在值類型中使用引用類型:
- 隨著
t1.info.weight
的改變,t
中的也改變了 - 所以說依舊是值拷貝,只不過是拷貝了引用類型數據的指針
- 這里的值傳遞是只傳遞了指針
那么真的這個引用類型會不會涉及到內存引用計數的管理呢?其實答案是肯定的,下面我們通過sil
代碼驗證一下:
通過sil
代碼我們可以看到strong_retain
和strong_release
的調用,所以說在值類型的內部使用引用類型依舊是需要通過引用計數管理的。
所以說,應該盡量避免這種值類型中使用引用類型的寫法,因為值類型的初衷就是為了不使用指針指向另一片內存區域,從而減少內存的使用,以提升效率。
3.2 值類型嵌套值類型
其實,在上面我們已經介紹過了,在Swift
中Int
的底層實現就是個結構體,所以也是值類型。
struct Teacher {
var age: Int = 18
}
值類型嵌套值類型:
- 在賦值的時候創建新的變量,兩者是獨立的。
- 嵌套的值類型變量也會創建新的變量,也可以說是深拷貝一份變量的值
3.3 引用類型嵌套引用類型
其實這也是我們經常用到的一種嵌套,比如類中嵌套類。
class Info {
var height: Int = 185
var weight: Double = 60.5
}
class Teacher {
var age: Int = 18
var name: String = "teacher1"
var info: Info = Info()
}
引用類型嵌套引用類型:
- 引用類型再賦值時創建了新的變量
- 新變量和源變量指向同一塊內存,內部引用類型變量也指向同一塊內存地址
- 改變引用類型嵌套的引用類型的值,也會影響到其他變量的值。
3.4 引用類型嵌套值類型
這個在上面我們也用到過,類中的Int
類型的屬性就是很好的例子。
class Teacher {
var age: Int = 28
}
引用類型嵌套值類型時:
- 賦值時創建了新的變量
- 新變量和源變量指向同一塊內存
- 改變源變量的內部值,會影響到其他變量的值