本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。
@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
關鍵字在一些除錯、日誌函式中也很有用, 比如我們可以看到在fatalError
和assert
函式的定義中也用到了@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
,這樣可以簡化方法呼叫者的工作。之前我們已經知道weak
和unownded
關鍵字可以被省略,下面舉一個例子說明self.
也可以被省略:
class Bar {
var i = 0
func some() {
doIt {
print(i) // 如果沒有@noescape,這裡需要寫self.i
}
}
}
複製程式碼
預設情況下,標記為@autoclosure
的閉包引數都是@noescape
的。在極少部分情況下,你也可以把引數標記為@autoclosure(escaping)
,這表示閉包在函式作用域之外依然生效,在寫非同步操作的程式碼時可能會用到。