本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。
在Swift中,初次接觸inout
關鍵字以及它的用法,可能會讓我們想起C/C++中的指標,但實際上Swift中inout
只不過是按值傳遞,然後再寫回原變數,而不是按引用傳遞:
An in-out parameter has a value that is passed in to the function, is modified by the function, and is passed back out of the function to replace the original value.
這樣的好處在於它遠比使用引用安全。首先舉個最簡單的例子看一看inout
關鍵字怎麼用:
func inc(inout i: Int) {
++i
}
var x = 0
inc(&x)
print(x) // 輸出結果:“1”
複製程式碼
引數x
傳入到inc
函式中後,在函式內被修改為1,函式返回時這個值(1)覆蓋了原來的x
的值(0),所以x
變成了1。
對比一下另一種同樣能在函式內部改變變數值的實現方式——閉包:
func inc() -> () -> Int {
var i = 0 //在inc函式內定義變數i
return { ++i } // 閉包中截獲變數i
}
let f = inc()
print(f()) // 輸出結果:“1”
print(f()) // 輸出結果:“2”
複製程式碼
閉包是通過截獲外部變數的引用從而實現對變數的修改的,我們通過閉包來證明,inout
引數是按值傳遞的:
func inc(inout i: Int) -> () -> Int {
return { ++i } // 閉包中截獲inout引數i
}
var x = 0
let f = inc(&x)
print(f()) // 輸出結果:“1”
print(x) // 輸出結果:“0”
複製程式碼
如果inout
引數是按引用傳遞,因為我們知道閉包會按引用截獲變數,所以閉包內的++i
語句實際上會影響到我們定義的變數x
,因此最後一個輸出的結果應該是1,但實際上執行結果是0。
這說明inout
引數是按值傳遞的,我們梳理一下整個過程:
- 首先變數
x
的值是0,它作為inout
引數傳入inc
方法中,inc
方法內有一個x
的副本,閉包截獲了這個副本的引用。 - 隨後
inc
方法方法返回,此時的副本值還是0,所以外部的變數x
的值為0。 - 接下來我們呼叫閉包,副本值被改為1,但是外部的變數
x
的值不會受到任何影響,所以它依然為0。
如果在inc
方法中返回閉包之前就呼叫這個閉包,那麼外部的變數x
的值就會被修改為1,這是因為在函式返回前,副本的值變成了1:
func inc(inout i: Int) -> () -> Int {
let f = { ++i } // 閉包中截獲inout引數i
f()
return f
}
var x = 0
let f = inc(&x)
print(x) // 輸出結果:“1”
複製程式碼
&並不總表示inout
如果在函式宣告中,引數是一個UnsafeMutablePointer
的指標,那麼傳遞引數的時候也要加上&
,這和inout
引數看上去用法類似,但實際上這裡是按引用傳遞而不是按值傳遞。我們可以改寫一下之前的inc
方法:
func inc(i: UnsafeMutablePointer<Int>) -> () -> Int {
//函式記憶體儲指標i的副本,閉包截獲這個副本
return {
i.memory++
return i.memory
}
}
複製程式碼
這個方法的使用與之前類似。有興趣的讀者可以自行嘗試。這裡我們換一種呼叫方式,傳入inc
方法的引數不是整數地址,而是陣列的地址:
let f: () -> Int
do {
var x = [0]
f = inc(&x)
}
print(f())
複製程式碼
為了與C相容,Swift中的陣列可以自動轉成指標,但由於陣列的地址不確定,所以這種寫法其實會產生一種未定義行為(UB)。
需要注意的是,使用&
時,也許我們用的是Swift的inout
語義,這是安全的,也許用的是UnsafeMutablePointer
的指標,這需要我們格外小心。我們需要考慮它所指向的物件的生命週期。在後面的章節中會更加詳細的討論這一點。