第六章——函式(自動閉包和記憶體)

bestswifter發表於2017-12-27

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

@autoclosure關鍵字

我們都很熟悉&&運算子,它是一個短路運算子。它有兩個運算元,首先左邊的運算元被處理,判斷是不是true,只有當它為true時才會繼續判斷右邊的運算元是不是true。這是因為根據&&運算子的特性,如果左邊的運算元是true,整個表示式的值才有可能為true。如果左邊的運算數為false,我們就短路這個運算,右邊的運算數也不會被訪問到。

比如,我們要對陣列的第一個元素進行某個判斷,程式碼可以這樣寫:

var evens = [1,2,3]
if !evens.isEmpty && evens[0] > 10 {
// 對evens[0]進行一些處理
}
複製程式碼

這種寫法依賴於&&運算子的短路機制,否則的話如果evnes為空陣列,程式就會崩潰。

雖然幾乎所有語言的&&||運算子都有內建的的短路機制,但如何為自定義的運算子新增短路機制呢。試想一下,如果我們這樣實現&&運算子:

func and(l: Bool, r: Bool) -> Bool {
guard l else { return false }
return r
}
複製程式碼

這樣的and方法不具備短路機制。因為被傳入的引數r的型別是Bool型別的,也就是類似於evens[0] > 10這樣的判斷,在and方法內部已經得出了結果。但我們希望的是把這個判斷過程延遲,只在引數l的值為true時才進行。這啟發我們用閉包來實現這個需求,因為閉包可以把真正要執行的程式碼封裝到一個變數中並延遲執行:

func and(l: Bool, _ r: () -> Bool ) -> Bool {
guard l else { return false }
return r()
}
複製程式碼

and方法應該這樣呼叫:

if and(!evens.isEmpty, {evens[0] > 10}) {
// 對evens[0]進行一些處理
}
複製程式碼

&&運算子相比,在and方法中,第二個引數變成了閉包,但其實我們關心的只是其中的內容:evens[0] > 10。使用閉包延遲了計算的發生,卻也給寫程式碼帶來了一些麻煩,有沒有什麼辦法既能延遲計算,又能書寫簡單呢?這時就需要@autoclosure關鍵字出馬了。

從字面來看,它表示“自動閉包”,也就是說它可以把一個表示式自動變成閉包的形式,我們修改一下and方法的定義,把閉包引數定義為@autoclosure

func and(l: Bool, @autoclosure _ r: () -> Bool ) -> Bool {
guard l else { return false }
return r()
}
複製程式碼

於是and方法的呼叫就可以略作簡化了:

if and(!evens.isEmpty, evens[0] > 10) {
// 對evens[0]進行一些處理
}
複製程式碼

除了可以用於短路運算子外,@autoclosure關鍵字在一些除錯、日誌函式中也很有用, 比如我們可以看到在fatalErrorassert函式的定義中也用到了@autoclosure關鍵字。以assert函式為例,它只在除錯時才會觸發,因此在Release模式下,閉包內的程式碼就不會執行。這樣有助於提高程式的效率。

@noescape關鍵字

使用閉包時我們需要格外注意記憶體管理問題,在使用捕獲列表時,我們有可能需要把self標記為weak,這樣閉包就不會持有對self的強引用。不過在使用類似於map這樣的函式時,我們從來不會把任何截獲的變數標記為weak,這是因為map是同步執行,這個閉包引數也不會被別的物件引用,所以不會產生迴圈引用問題。我們還看一下map函式的第一個引數,它被標記為@noescape

@noescape transform: (Self.Generator.Element) throws -> T
複製程式碼

@noescape表示閉包的作用域不會超出map函式。一旦map函式執行結束,閉包就不會再被引用。也就是說,編譯器確保不會在非同步呼叫的回撥函式中使用這個閉包,也不會有全域性變數或物件的屬性持有這個閉包。這樣一來,編譯器可以對閉包做一些微小的優化,方法的呼叫者不用擔心記憶體管理問題,我們可以像平時那樣寫程式碼,沒有捕獲列表和weak關鍵字,訪問方法呼叫者的屬性時也不要加上self.

舉個例子來看一下,我們首先定義兩個函式,這兩個函式的引數code都是被標記成@noescape的閉包:

func doIt(@noescape code: () -> ()) {}

func doItMore(@noescape code: () -> ()) {}
複製程式碼

doIt函式內部,有些針對code的操作是允許的,有些操作是不允許的:

func doIt(@noescape code: () -> ()) {
/* 下面的三行程式碼都是可行的 */

code()	// 直接呼叫這個閉包,當然是可以的
doItMore(code)	// 把它作為引數,傳入另一個函式doItMore中也是可以的。前提是doItMore函式中把引數宣告為@noescape
doItMore {
code()	// 在被標記為@noescape的閉包中截獲code閉包也是可以的
}

/* 下面的三行程式碼會導致編譯錯誤 */

// code作為引數傳到了另一個函式中,而dispatch_async函式並沒有把他的閉包引數定義為@noescape
dispatch_async(dispatch_get_main_queue(), code)
let _code:() -> () = code	// 不能用變數來儲存@noescape閉包
let __code = { code() }	// 不能在沒被標記成@noescape的閉包中截獲code閉包
}
複製程式碼

總的來說,@noescape關鍵字定義的是閉包引數的使用規則,而非這個閉包自身如何實現。因為閉包引數不能被臨時儲存,也不能被非同步呼叫,所以這就決定了它的作用域只侷限於函式內部。

在我們自己實現一個接收閉包作為引數的函式時,如果可能的話應該把這個閉包標記為@noescape,這樣可以簡化方法呼叫者的工作。之前我們已經知道weakunownded關鍵字可以被省略,下面舉一個例子說明self.也可以被省略:

class Bar {
var i = 0
func some() {
doIt {
print(i) // 如果沒有@noescape,這裡需要寫self.i
}
}
}
複製程式碼

預設情況下,標記為@autoclosure的閉包引數都是@noescape的。在極少部分情況下,你也可以把引數標記為@autoclosure(escaping),這表示閉包在函式作用域之外依然生效,在寫非同步操作的程式碼時可能會用到。

相關文章