第六章——函式(inout引數與變異方法)

bestswifter發表於2017-12-27

本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。

在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的指標,這需要我們格外小心。我們需要考慮它所指向的物件的生命週期。在後面的章節中會更加詳細的討論這一點。

相關文章