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到外面。