Swift值類(lèi)型&引用類(lèi)型
前言
值類(lèi)型和引用類(lèi)型是Swift中兩種數(shù)據(jù)存儲(chǔ)方式,簡(jiǎn)單來(lái)說(shuō)值類(lèi)型就是直接存儲(chǔ)的值,引用類(lèi)型就是存儲(chǔ)的指針,在談值類(lèi)型和引用類(lèi)型前可能你需要了解一些關(guān)于內(nèi)存和Mach-O的知識(shí)。下面放上我以前寫(xiě)過(guò)的幾篇文章,僅供參考。
iOS內(nèi)存五大區(qū)
iOS 中的虛擬內(nèi)存和物理內(nèi)存
Mach-O探索
簡(jiǎn)單來(lái)說(shuō)值類(lèi)型可以理解為存儲(chǔ)在棧區(qū)或者全局區(qū),引用類(lèi)型一般存儲(chǔ)在堆區(qū),下面我們來(lái)看個(gè)簡(jiǎn)單的例子。

我們可以看到a和t的地址都是在棧區(qū),因?yàn)闂^(qū)通常都是0x7開(kāi)頭。但是a中存儲(chǔ)的直接就是18這個(gè)值,t中存儲(chǔ)的是個(gè)全局區(qū)的指針。這就是最簡(jiǎn)單的值類(lèi)型和引用類(lèi)型的區(qū)別。
1. 值類(lèi)型
值類(lèi)型,即每個(gè)實(shí)例保持一份數(shù)據(jù)拷貝。
在 Swift 中,struct,enum,以及 tuple 都是值類(lèi)型。而平時(shí)使用的 Int、Double、Float、String、Array、Dictionary、Set 其實(shí)都是用結(jié)構(gòu)體實(shí)現(xiàn)的,也是值類(lèi)型。
Swift 中,值類(lèi)型的賦值為深拷貝(Deep Copy),值語(yǔ)義(Value Semantics)即新對(duì)象和源對(duì)象是獨(dú)立的,當(dāng)改變新對(duì)象的屬性,源對(duì)象不會(huì)受到影響,反之同理。
雖然說(shuō)Int、Double、Float、String、Array、Dictionary、Set時(shí)使用結(jié)構(gòu)體實(shí)現(xiàn)的,所以也是值類(lèi)型,但是就我個(gè)人理解來(lái)說(shuō),這些作為值類(lèi)型好像就是那么理所當(dāng)然的,當(dāng)然對(duì)于很長(zhǎng)的String還是會(huì)通過(guò)存儲(chǔ)指向堆區(qū)的指針來(lái)實(shí)現(xiàn),當(dāng)然也會(huì)通過(guò)TaggedPointer等技術(shù)進(jìn)行優(yōu)化,這里大體還是和OC相同的,感興趣的可以看看我的另一篇文章iOS Objective-C 內(nèi)存管理。說(shuō)了這么多,其實(shí)我們糾結(jié)的一個(gè)問(wèn)題就是struct為什么是值類(lèi)型,下面我們就來(lái)探索一番。
1.1 struct 為什么是值類(lèi)型
1.1.1 結(jié)構(gòu)體和類(lèi)的區(qū)別
從代碼看區(qū)別
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)
通過(guò)以上的代碼我們可以知道:
- 類(lèi)中的屬性需要使用
?、!或者賦初始值才不會(huì)導(dǎo)致編譯報(bào)錯(cuò) - 結(jié)構(gòu)體中的屬性不需要賦初始值,也不用使用
?、! - 結(jié)構(gòu)體的初始化需要同時(shí)初始化結(jié)果圖內(nèi)部的屬性
- 類(lèi)的初始化可以不用初始化類(lèi)中的屬性
- 結(jié)構(gòu)體中的
optional屬性,或者賦值的屬性可以不在結(jié)構(gòu)體初始化的時(shí)候初始化
從sil代碼看區(qū)別
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)
}
通過(guò)sil代碼我們可以看到:
- 類(lèi)中如果不實(shí)現(xiàn)自定義
init方法就會(huì)有個(gè)init()方法 - 結(jié)構(gòu)體中會(huì)提供默認(rèn)的初始化方法
1.1.2 驗(yàn)證結(jié)構(gòu)體是值類(lèi)型
定義一個(gè)結(jié)構(gòu)體:
struct Teacher {
var age: Int
var age1: Int
}
var t = Teacher(age: 18, age1: 20)
使用lldb調(diào)試:

此時(shí)我們可以看到,結(jié)構(gòu)體內(nèi)部直接存儲(chǔ)的就是結(jié)構(gòu)體中的屬性的值。所以說(shuō)結(jié)構(gòu)體是值類(lèi)型是沒(méi)問(wèn)題的。
1.1.3 驗(yàn)證結(jié)構(gòu)體是值拷貝
此時(shí)我們創(chuàng)建個(gè)新的實(shí)例變量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的值后我們發(fā)現(xiàn)t中的數(shù)據(jù)并沒(méi)有改變,所以說(shuō)t和t1之間是值傳遞,即t和t1是存儲(chǔ)在不同內(nèi)存空間的,在var t1 = t時(shí),是將t中的值,拷貝到t1中,t1修改時(shí),只會(huì)修改自己內(nèi)存中的數(shù)據(jù),是不會(huì)影響到t的內(nèi)存的。
另外在打印兩個(gè)實(shí)例變量地址的時(shí)候也明顯不是一樣的。
1.1.4 通過(guò)sil驗(yàn)證struct是值類(lèi)型
我們查看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方法中并沒(méi)有調(diào)用malloc相關(guān)的開(kāi)辟內(nèi)存的方法,這里也是只是將傳入的兩個(gè)值賦給初始化的結(jié)構(gòu)體而已。
1.1.5 常量值類(lèi)型
如果聲明一個(gè)值類(lèi)型的常量,那么就意味著該常量是不可變的(無(wú)論內(nèi)部數(shù)據(jù)為 var還是let)。

1.1.6 小結(jié)
至此我們就驗(yàn)證了結(jié)構(gòu)體是值類(lèi)型:
- 結(jié)構(gòu)體不像類(lèi)一樣需要調(diào)用
malloc等方法去開(kāi)辟內(nèi)存空間 - 結(jié)構(gòu)體的內(nèi)存中直接存儲(chǔ)值
- 值類(lèi)型的賦值是一個(gè)值傳遞的過(guò)程,相當(dāng)于深拷貝
1.2 其他
關(guān)于enum和tuple這里就不一一分析了,在后續(xù)的篇章中會(huì)陸續(xù)提到。
2. 引用類(lèi)型
引用類(lèi)型,即所有實(shí)例共享一份數(shù)據(jù)拷貝。
在 Swift 中,class 和closure是引用類(lèi)型。引用類(lèi)型的賦值是淺拷貝(Shallow Copy),引用語(yǔ)義(Reference Semantics)即新對(duì)象和源對(duì)象的變量名不同,但其引用(指向的內(nèi)存空間)是一樣的,因此當(dāng)使用新對(duì)象操作其內(nèi)部數(shù)據(jù)時(shí),源對(duì)象的內(nèi)部數(shù)據(jù)也會(huì)受到影響。
2.1 驗(yàn)證類(lèi)是引用類(lèi)型
定義一個(gè)類(lèi)
class Teacher {
var age: Int = 28
var age1: Int = 20
}
var t = Teacher()
print("end")
lldb調(diào)試

從lldb調(diào)試中我們可以看到,類(lèi)實(shí)例對(duì)象指針內(nèi)部存儲(chǔ)的是一個(gè)指向全局區(qū)的指針,而這塊內(nèi)存區(qū)域才是存儲(chǔ)的真正的實(shí)例變量的信息,所以說(shuō)類(lèi)是個(gè)引用類(lèi)型。
2.2 驗(yàn)證類(lèi)對(duì)象是指針拷貝
我們使用如下代碼進(jìn)行驗(yàn)證:
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")

通過(guò)打印結(jié)果我們可以知道,雖然我們修改的是t1這個(gè)實(shí)例對(duì)象中age的值,但是當(dāng)我們打印t這個(gè)實(shí)例變量的age的值的時(shí)候也隨之改變了,所以我們就能夠確定類(lèi)對(duì)象之間是指針拷貝,并且在內(nèi)存地址的打印中我們也可以清晰的看見(jiàn),它們指向同一片內(nèi)存空間,一個(gè)改變則全部都改變。
2.4 通過(guò)sil進(jìn)一步驗(yàn)證類(lèi)的引用類(lèi)型
其實(shí)到這里也就沒(méi)什么好說(shuō)的的了,在類(lèi)的初始化的時(shí)候肯定是會(huì)調(diào)用alloc方法來(lái)開(kāi)辟內(nèi)存空間的,這里借著上面的sil代碼,我們來(lái)看看Info這個(gè)類(lèi)的Info.__allocating_init()方法吧:

這里首先就調(diào)用了alloc_ref為Info初始化一塊內(nèi)存空間。
2.5 常量引用類(lèi)型
如果聲明一個(gè)引用類(lèi)型的常量,那么就意味著該常量的引用不能改變(即不能被同類(lèi)型變量賦值),但指向的內(nèi)存中所存儲(chǔ)的變量是可以改變的,示例如下:

此處是不會(huì)報(bào)編譯錯(cuò)誤的,這點(diǎn)與值類(lèi)型也是不同的。
2.6 小結(jié)
至此我們就驗(yàn)證了類(lèi)是引用類(lèi)型:
- 類(lèi)需要調(diào)用
alloc等方法去開(kāi)辟內(nèi)存空間 - 類(lèi)的實(shí)例對(duì)象中存儲(chǔ)的是指針地址,這個(gè)地址中存儲(chǔ)的才是值
- 類(lèi)的實(shí)例對(duì)象的賦值是一個(gè)指針拷貝的過(guò)程,相當(dāng)于淺拷貝
3. 嵌套類(lèi)型
所謂嵌套類(lèi)型就是引用類(lèi)型中有值類(lèi)型,或者值類(lèi)型中有引用類(lèi)型,其實(shí)在上面的例子中已經(jīng)涉及到了,下面我們通過(guò)兩兩組合,分四種情況來(lái)簡(jiǎn)單介紹一下。
3.1 值類(lèi)型嵌套引用類(lèi)型
這里是在結(jié)構(gòu)體中添加一個(gè)引用類(lèi)型的屬性,示例代碼如下:
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")

我們可以看到,在值類(lèi)型中使用引用類(lèi)型:
- 隨著
t1.info.weight的改變,t中的也改變了 - 所以說(shuō)依舊是值拷貝,只不過(guò)是拷貝了引用類(lèi)型數(shù)據(jù)的指針
- 這里的值傳遞是只傳遞了指針
那么真的這個(gè)引用類(lèi)型會(huì)不會(huì)涉及到內(nèi)存引用計(jì)數(shù)的管理呢?其實(shí)答案是肯定的,下面我們通過(guò)sil代碼驗(yàn)證一下:

通過(guò)sil代碼我們可以看到strong_retain和strong_release的調(diào)用,所以說(shuō)在值類(lèi)型的內(nèi)部使用引用類(lèi)型依舊是需要通過(guò)引用計(jì)數(shù)管理的。
所以說(shuō),應(yīng)該盡量避免這種值類(lèi)型中使用引用類(lèi)型的寫(xiě)法,因?yàn)橹殿?lèi)型的初衷就是為了不使用指針指向另一片內(nèi)存區(qū)域,從而減少內(nèi)存的使用,以提升效率。
3.2 值類(lèi)型嵌套值類(lèi)型
其實(shí),在上面我們已經(jīng)介紹過(guò)了,在Swift中Int的底層實(shí)現(xiàn)就是個(gè)結(jié)構(gòu)體,所以也是值類(lèi)型。
struct Teacher {
var age: Int = 18
}
值類(lèi)型嵌套值類(lèi)型:
- 在賦值的時(shí)候創(chuàng)建新的變量,兩者是獨(dú)立的。
- 嵌套的值類(lèi)型變量也會(huì)創(chuàng)建新的變量,也可以說(shuō)是深拷貝一份變量的值
3.3 引用類(lèi)型嵌套引用類(lèi)型
其實(shí)這也是我們經(jīng)常用到的一種嵌套,比如類(lèi)中嵌套類(lèi)。
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()
}
引用類(lèi)型嵌套引用類(lèi)型:
- 引用類(lèi)型再賦值時(shí)創(chuàng)建了新的變量
- 新變量和源變量指向同一塊內(nèi)存,內(nèi)部引用類(lèi)型變量也指向同一塊內(nèi)存地址
- 改變引用類(lèi)型嵌套的引用類(lèi)型的值,也會(huì)影響到其他變量的值。
3.4 引用類(lèi)型嵌套值類(lèi)型
這個(gè)在上面我們也用到過(guò),類(lèi)中的Int類(lèi)型的屬性就是很好的例子。
class Teacher {
var age: Int = 28
}
引用類(lèi)型嵌套值類(lèi)型時(shí):
- 賦值時(shí)創(chuàng)建了新的變量
- 新變量和源變量指向同一塊內(nèi)存
- 改變?cè)醋兞康膬?nèi)部值,會(huì)影響到其他變量的值