Swift 中的屬性分為兩大類:存儲(chǔ)屬性 , 計(jì)算屬性
一: 存儲(chǔ)屬性
存儲(chǔ)屬性類似于成員變量,定義方式很簡(jiǎn)單:
//存儲(chǔ)屬性
class Person{
var name: String = "張三"
}
存儲(chǔ)屬性存儲(chǔ)在成員變量中;結(jié)構(gòu)體Struct
和類Class
都可以定義存儲(chǔ)屬性,唯獨(dú)枚舉不可以.因?yàn)槊杜e變量的內(nèi)存中只用于存儲(chǔ)
case 值和關(guān)聯(lián)值,沒有用來(lái)存儲(chǔ)存儲(chǔ)屬性的內(nèi)存.
另外需要注意的是,在初始化類和結(jié)構(gòu)體的實(shí)例時(shí),必須為所有的存儲(chǔ)屬性設(shè)置初始值.
二: 計(jì)算屬性
計(jì)算屬性的定義方式需要用到set , get
關(guān)鍵字:
//計(jì)算屬性
var age: Int{
set{
...
}
get{
...
}
}
像上面的age
就是一個(gè)計(jì)算屬性.為了更生動(dòng)的理解計(jì)算屬性,我們以游戲賬號(hào)升級(jí)為例子.假如規(guī)則是這樣:在線兩個(gè)小時(shí),游戲賬號(hào)升級(jí)一級(jí):
//計(jì)算屬性
struct Game{
//游戲時(shí)長(zhǎng),單位 : 小時(shí)
var time: Int
//游戲等級(jí),在線兩個(gè)小時(shí)升一級(jí)
var grade: Int {
set{
time = newValue * 2
}
get{
time / 2
}
}
}
//初始化 游戲時(shí)長(zhǎng)為 2 個(gè)小時(shí)
var game1 = Game(time: 2)
print("在線 \(game1.time) 個(gè)小時(shí), 游戲等級(jí)為 \(game1.grade) 級(jí)")
//設(shè)置游戲等級(jí)為 30 級(jí)
game1.grade = 30
print("在線 \(game1.time) 個(gè)小時(shí), 游戲等級(jí)為 \(game1.grade) 級(jí)")
計(jì)算屬性的本質(zhì)就是方法,這一點(diǎn)我們可以通過(guò)匯編直接看出來(lái):
既然計(jì)算屬性的本質(zhì)是方法,就說(shuō)明計(jì)算屬性是不會(huì)占用實(shí)例變量的內(nèi)存.因?yàn)槊杜e,結(jié)構(gòu)體,類中都可以定義方法,所以同樣也可以定義計(jì)算屬性.需要注意的是,計(jì)算屬性只能用
var,不能用
let.
因?yàn)橛?jì)算屬性是會(huì)變化的.
枚舉rawValue
的本質(zhì)
我們之前在從零學(xué)習(xí)Swift 02:枚舉和可選項(xiàng) 中說(shuō)過(guò),枚舉是不會(huì)存儲(chǔ)原始值的,今天我們就來(lái)搞清楚枚舉rawValue
的本質(zhì).
首先看一下系統(tǒng)默認(rèn)的rawValue
:
我們可以寫個(gè)函數(shù),實(shí)現(xiàn)rawValue
的功能:
但是很奇怪,為什么系統(tǒng)的rawValue
沒有括號(hào)()
,其實(shí)它是計(jì)算屬性
現(xiàn)在我們就搞清楚了
rawValue的本質(zhì)其實(shí)就是只讀的,計(jì)算屬性
另外我們也發(fā)現(xiàn),計(jì)算屬性可以只有get
沒有set
,那可不可以只有set
,沒有get
呢?不可以,編譯器會(huì)直接報(bào)錯(cuò).
三: 延遲存儲(chǔ)屬性
使用lazy
可以定義一個(gè)延遲存儲(chǔ)屬性,在第一次使用屬性的時(shí)候才會(huì)初始化.類似于 OC 中的懶加載.
如上圖只創(chuàng)建了person
,還沒有使用car
,car
就已經(jīng)初始化了.
我們?cè)?code>car前面加上lazy
關(guān)鍵字:
- 使用
lazy
延遲存儲(chǔ)屬性時(shí)要注意一下幾點(diǎn):
-
lazy
屬性必須是var
,不能是let
.因?yàn)?code>Swift規(guī)定let
必須在實(shí)例初始化方法完成之前就有值.而lazy
是用到的時(shí)候才初始化,這就沖突了. -
lazy
屬性不是線程安全的,多個(gè)線程同時(shí)訪問同一個(gè)lazy
屬性,可能不止加載一次. - 當(dāng)結(jié)構(gòu)體包含一個(gè)延遲存儲(chǔ)屬性時(shí),只有
var
修飾的實(shí)例才能訪問延遲存儲(chǔ)屬性,let
修飾的實(shí)例不允許訪問延遲存儲(chǔ)屬性,什么意思呢?看下面一張圖就知道了:
結(jié)構(gòu)體 let 實(shí)例
四: 屬性觀察器
屬性觀察器類似于 OC 中的 KVO
,它的定義方式如下:
使用屬性觀察器必須滿足三個(gè)條件:
- 必須是非
lazy
修飾(因?yàn)?lazy 屬性是在第一次訪問屬性的時(shí)候才創(chuàng)建的,而添加屬性觀察器可能會(huì)打破 lazy 的機(jī)制
) - 必須是
var
變量(既然是屬性觀察器,肯定是觀察屬性值的變化,如果用 let 常量就沒有任何意義了
) - 必須是存儲(chǔ)屬性(
因?yàn)橛?jì)算屬性內(nèi)部本來(lái)就有一個(gè)
set,可以把監(jiān)聽代碼寫到
set中.
)
思考一下為什么計(jì)算屬性不能設(shè)置屬性觀察器?
因?yàn)橛?jì)算屬性內(nèi)部本來(lái)就有一個(gè)set
,可以把監(jiān)聽代碼寫到set
中.
五: inout 參數(shù)
之前在從零學(xué)習(xí)Swift 01:了解基礎(chǔ)語(yǔ)法中用匯編分析過(guò)inout
參數(shù),知道inout
輸入輸出參數(shù)是引用傳遞.今天使用更復(fù)雜的類型更深入的研究inout
參數(shù)的本質(zhì).
示例代碼:
//矩形
struct Rectangle{
//存儲(chǔ)屬性 長(zhǎng)
var length: Int
//屬性觀察器 寬
var width: Int{
willSet{
print("newValue : ",newValue)
}
didSet{
print("newValue : \(width) , oldValue : \(oldValue)")
}
}
//計(jì)算屬性 (計(jì)算屬性不占用實(shí)例內(nèi)存空間,本質(zhì)是方法)
//面積
var area: Int{
set{
length = newValue / width
}
get{
return length * width
}
}
func show(){
print("長(zhǎng)方形的長(zhǎng) length = \(length) , 寬 width = \(width) , 面積 area = \(area)")
}
}
var rect = Rectangle(length: 10, width: 4)
rect.show()
func test(_ num: inout Int){
num = 20
}
test(&rect.length)
rect.show()
上面代碼結(jié)構(gòu)體中分別有存儲(chǔ)屬性,屬性觀察器,計(jì)算屬性.下面我們就分別把這三種屬性傳入inout
參數(shù).
-
inout
參數(shù)之存儲(chǔ)屬性
分析匯編:
從上圖可以看到,調(diào)用test()
時(shí)直接把全局變量rect
的地址作為參數(shù)傳入進(jìn)去.為什么不是把length
的地址傳進(jìn)去呢?因?yàn)?code>length是結(jié)構(gòu)體的第一個(gè)成員,所以結(jié)構(gòu)體的地址就是length
的地址.這里傳入rect
的地址和length
地址是等價(jià)的.
-
inout
參數(shù)之計(jì)算屬性
分析匯編:
從匯編語(yǔ)言中可以看到,當(dāng)inout
參數(shù)傳入的是計(jì)算屬性時(shí),在調(diào)用test()
方法之前會(huì)先調(diào)用計(jì)算屬性的getter
方法取出值,并且把值存入棧空間;然后再調(diào)用test()
方法,并且把棧空間的地址作為參數(shù)傳遞進(jìn)去.所以在test()
方法內(nèi)部修改的是棧空間的值;最后再調(diào)用計(jì)算屬性的setter
方法,從棧空間中取出值傳入setter
方法,并賦值.
圖解:
3.inout
參數(shù)之屬性觀察器
分析匯編:
從上圖的匯編中可以看到,inout
參數(shù)是屬性觀察器時(shí),內(nèi)部邏輯和計(jì)算屬性很相似,都是取出值放到棧空間,然后修改棧空間的值.
為什么屬性觀察器不能像存儲(chǔ)屬性那樣,直接傳入地址,直接修改呢?因?yàn)閷傩杂^察器涉及到監(jiān)聽的邏輯.我們看看第三步的setter
方法的匯編:
可以看到setter
方法內(nèi)部會(huì)調(diào)用willSet , didSet
,并且在willSet
調(diào)用完之后才真正賦值.
屬性觀察器之所以要這么設(shè)計(jì)就是因?yàn)橐{(diào)用willSet
和didSet
.達(dá)到監(jiān)聽屬性改變的效果.因?yàn)?code>inout參數(shù)就是引用傳遞.如果直接把width
的地址傳給test()
,test
內(nèi)部就直接修改了width
的值.willSet
和didSet
根本就不會(huì)觸發(fā).
現(xiàn)在我們總結(jié)一下inout
:
-
inout
本質(zhì)就是引用傳遞 - 當(dāng)
inout
參數(shù)是計(jì)算屬性或者設(shè)置了屬性觀察器的存儲(chǔ)屬性時(shí),采取了copy in , copy out
的做法:
2.1: 調(diào)用函數(shù)時(shí)先復(fù)制參數(shù)的值,產(chǎn)生副本( copy in )
2.2: 將副本的內(nèi)存地址傳入函數(shù),在函數(shù)內(nèi)修改的是副本的值
2.3: 將副本的值取出來(lái),覆蓋實(shí)參的值( copy out )
六: 類型屬性
上面講的屬性都是實(shí)例屬性,通過(guò)實(shí)例訪問的.Swift 中還有通過(guò)類型訪問的屬性--類型屬性.
類型屬性通過(guò)static
關(guān)鍵字定義;如果是類,也可以通過(guò)class
關(guān)鍵字定義.
//類型屬性
struct Person{
static var age: Int = 1
}
Person.age = 10
類型屬性的本質(zhì)就是全局變量,在整個(gè)程序運(yùn)行過(guò)程中,只有1份內(nèi)存.
我們用匯編看一下以下代碼num1 , age , num2
的內(nèi)存地址:
var num1 = 10
struct Person{
static var age: Int = 1
}
Person.age = 11
var num2 = 12
匯編如下:
會(huì)發(fā)現(xiàn)num1 , age , num2
三個(gè)變量的地址都是連續(xù)的,說(shuō)明他們都在全局區(qū).
類型屬性還有個(gè)很重要的特性:類型屬性默認(rèn)是 lazy , 在第一次使用的時(shí)候才會(huì)初始化, 并且是線程安全的,只會(huì)初始化一次.
前面我們講延遲存儲(chǔ)屬性 lazy 關(guān)鍵字
時(shí)說(shuō)過(guò),lazy
不是線程安全的.為什么類型屬性默認(rèn)是lazy
,它為什么是線程安全的呢?
因?yàn)樗膬?nèi)部會(huì)調(diào)用swift_once dispatch_once_f
下面我們通過(guò)匯編證明一下,首先斷點(diǎn)打到類型屬性初始化的部分,看看類型屬性初始化的函數(shù)地址.
然后運(yùn)行程序,看到類型屬性初始化函數(shù)地址為:
接著把斷點(diǎn)調(diào)整到如圖所示位置:
運(yùn)行代碼,分析匯編如下:
進(jìn)入函數(shù):
進(jìn)入swift_once
:
會(huì)發(fā)現(xiàn)swift_once
內(nèi)部會(huì)調(diào)用dispatch_once_f
.
所以現(xiàn)在就能明白為什么類型屬性是線程安全的了,因?yàn)樗某跏蓟a放到dispatch_once_f
中調(diào)用的.