inout是可以用來在函式內部修改外部屬性記憶體的,
一、inout回顧
示例代碼:
func test(_ num: inout Int) {
num = 20
}
var a = 10
test(&a)
print(a) // 輸出:20
test(&a)

通過匯編分析,全域變數a的地址0x6c52(%rip)傳遞給了暫存器rdi,rdi作為引數傳遞給了test函式,所以inout的本質就是參考傳遞(地址傳遞),
二、inout本質
示例代碼:
struct Shape {
var width: Int
var side: Int {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, side)
}
}
var girth: Int {
set {
print("setGirth")
width = newValue / side
}
get {
print("getGirth")
return width * side
}
}
func show() {
print("width=\(width), side=\(side), girth=\(girth)")
}
}
func test(_ num: inout Int) {
print("test")
num = 20
}
var s = Shape(width: 10, side: 4)
2.1. 存盤屬性
test(&s.width)
s.show()
/*
輸出:
test
getGirth
width=20, side=4, girth=80
*/

分析:
0x6c9d(%rip)是全域變數s的地址值;s的記憶體地址和結構體Shape中第一個存盤屬性的地址是相同的(值型別);- 相當于把實體
s中存盤屬性width的記憶體地址傳給了test函式; - 所以結構體的存盤屬性使用
inout的本質和全域/區域變數都一樣,
結論:
由于存盤屬性有自己的記憶體地址,所以直接把存盤屬性的地址傳遞給需要修改的函式,在函式內部修改存盤屬性的值,
2.2. 計算屬性
test(&s.girth)
s.show()
/*
輸出:
getGirth
test
setGirth
getGirth
width=5, side=4, girth=20
*/
> 思考:上面的代碼中s.girth也是地址傳遞么?答案:不是,因為girth不是存盤屬性,所以不占用結構體的記憶體,但是使用&s.girth不會報錯,并且正常讀寫值,所以編譯器是允許我們這么做的,那它是如何傳遞修改值的呢?

分析:
- 執行代碼
test(&s.girth)首先呼叫了girth的getter方法; - 然后
getter方法會回傳一個值,這個值放在臨時空間內(區域變數); - 呼叫
test方法時是把getter回傳的臨時變數作為引數傳遞的(傳遞的還是地址值),這時候在test方法內部修改的是臨時變數記憶體的值; - 當修改區域變數記憶體時,會呼叫
girth的setter方法,把區域變數的值作為引數傳遞; - 最終的結果就是值被修改了,
結論:
由于計算屬性沒有自己的地址值,所以會呼叫getter方法獲取一個區域變數,把區域變數的值傳遞給需要修改的函式,在函式內部修改區域變數的值,最后把區域變數的值傳遞給setter方法,
2.3. 屬性觀察器
test(&s.side)
s.show()
/*
輸出:
test
willSet 20
didSet 4 20
getGirth
width=10, side=20, girth=200
*/



分析:
- 取出
0x6cc3(%rip)的前8個位元組給rax,而0x6cc3(%rip)的本質就是存盤屬性side(通過匯編注釋可以看出s偏移8個位元組,而width占用8個位元組,跳過width就是side); rax的值又給了區域變數-0x28(%rbp);- 然后把區域變數
rdi的值傳遞給了test函式,通過列印發現rdi保存的值就是20; test函式執行完成后,開始執行side的setter方法,并把之前的區域變數rdi作為引數傳遞過去;willset之前沒有修改rdi,所以rdi保存的還是20,并且作為第一個引數傳遞給了willset;- 由于
willset之后才會真正修改屬性值,并且didset之前已經知道修改過的屬性值,所以真正修改屬性值是在willset和didset之間;
結論:
修改帶有屬性觀察器的存盤屬性值時,和計算屬性的程序有點類似,先拿到屬性的值給區域變數,然后把區域變數的地址值傳遞給需要修改的函式,函式內部會修改區域變數的值,函式執行完成后把已經修改過的區域變數的值賦值給屬性,賦值時,優先執行屬性的willset方法,willset執行結束后,才會真正修改屬性的值,最后呼叫didset,
小技巧:需要傳遞
inout引數的函式,業務邏輯是非常獨立的,目的僅僅是修改傳遞過來的引數值,不會影響計算屬性/存盤屬性(屬性觀察器)的邏輯,所以除了計算屬性可以直接傳地址,其他屬性都需要一個區域變數做一個中轉,
2.4. inout的本質總結
-
如果實參有物理記憶體地址,且沒有設定屬性觀察器
- 直接將實參的記憶體地址傳入函式(實參進行參考傳遞)
-
如果實參是計算屬性或設定了屬性觀察器,采取Copy In Copy Out的做法:
- 呼叫該函式時,先復制實參的值,產生一個副本(區域變數-執行
get方法) - 將副本的記憶體地址傳入函式(副本進行參考傳遞),在函式內部可以修改副本的值
- 函式回傳后,再將副本的值覆寫實參的值(執行
set方法)
- 呼叫該函式時,先復制實參的值,產生一個副本(區域變數-執行
總結:inout的本質就是參考傳遞(地址傳遞),
什么是Copy In Copy Out?先Copy到函式里,修改后再Copy到外面,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/286150.html
標籤:iOS
上一篇:Swift系列九 - 屬性
下一篇:SwiftUI 簡明教程之格子
