1.閉包
閉包是一個捕獲了外部變量或者常量的函數,可以有名字的函數,可以是匿名的函數,也可以是不捕獲外部變量的函數。是以可以說閉包是特殊的函數。
閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 ObjC 中的代碼塊(blocks)比較相似。
捕獲的變量,可以寫在捕獲清單裡. 如果使用捕獲清單,即使省略了參數名字、參數類型、傳回類型,也必須要用 in 的關鍵字
●捕獲清單裡面的是捕獲的是值,不可變,
●未在捕獲清單裡,捕獲到的是位址,可以修改
案例1: 閉包内部不能修改值傳遞的

案例2: 閉包外部修改值傳遞的資料,也是無效的
2. 閉包表達式
下面這種,是我們熟知的閉包表達式:一個匿名函數 + 捕獲了外部的變量或者常量:
{ (age: Int) -> Int in
return age + 1
}
閉包表達式還可以按照下面的規則,變的更加簡潔:
●如果參數及傳回值類型可以根據上下文推斷出來,則可以省略參數 / 傳回值的類型
●如果隻有一行,是單表達式,可以省略 return 關鍵字
●參數的名稱可以根據參數的位置,簡寫成 $0、$1
●如果最後一個參數是閉包,可以使用尾随閉包表達式
●如果隻有一個參數,且是閉包,可以省略小括号,直接寫尾随閉包
var array = [1, 2, 3]
// 正常寫法
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1 < item2 })
// 省略參數類型
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
// 省略傳回值類型
array.sort(by: {(item1, item2) in return item1 < item2 })
// 省略 return 關鍵字
array.sort(by: {(item1, item2) in item1 < item2 })
// 省略小括号(尾随閉包)
array.sort{(item1, item2) in $0 < $1 }
// 省略參數,使用預設的$0 $1
array.sort{ $0 < $1 }
// 甚至整個閉包表達式,隻剩下一個 < 符号
array.sort(by: <)
閉包可以當做類型使用,也就是可以用來定義變量、參數、傳回值
// 定義變量
var closure : ((Int) -> Int)?
closure = { (age: Int) -> Int in
return age + 1
}
// 定義參數
func test(param: ((Int) -> Int)) {
print(param())
}
// 作為傳回值
func getClosure() -> ((Int) -> Int) {
var closure : ((Int) -> Int) = { (age: Int) -> Int in
return age + 1
}
return closure
}
3.尾随閉包
當函數的最後一個參數是閉包時,可以使用尾随閉包來增強函數的可讀性。在使用尾随閉包時,你不用寫出它的參數标簽:
func test(closure: () -> Void) {
...
}
// 不使用尾随閉包
test(closure: {
...
})
// 使用尾随閉包
test() {
...
}
4.逃逸閉包
逃逸閉包是指閉包作為參數傳入函數中,然而它的生命周期比函數的聲明周期還長,也就是閉包需要在函數釋放後也可以調用,我們就稱這個閉包為逃逸閉包。編譯器預設閉包為非逃逸閉包,用@nonescaping修飾。逃逸閉包使用@escaping 修飾。
var completions: [() -> Void] = []
func testClosure(completion: () -> Void) {
completions.append(completion)
}
此時編譯器會報錯,提示你這是一個逃逸閉包,我們可以在參數名之前标注 @escaping,用來指明這個閉包是允許“逃逸”出這個函數的。
var completions: [() -> Void] = []
func testEscapingClosure(completion: @escaping () -> Void) {
completions.append(completion)
}
另外,将一個閉包标記為 @escaping 意味着你必須在閉包中顯式地引用 self,而非逃逸閉包則不用。這提醒你可能會一不小心就捕獲了self,注意循環引用。
有兩種情況需要使用逃逸閉包:
●閉包被指派給屬性或者成員變量
●在延時後,使用閉包
4.1 閉包被指派給屬性或者成員變量
class LGTeacher{
var age = 18
var complitionHandler: ((Int)->Void)?
func makeIncrementer(amount: Int, handler: @escaping (Int) -> Void){
var runningTotal = 0
runningTotal += amount
self.complitionHandler = handler
}
func doSomething(){
self.makeIncrementer(amount: 10) {
//會引起循環引用
// self.age = 20
print($0)
}
}
deinit {
print("LGTeaher deinit")
}
}
func test() {
let t = LGTeacher()
t.doSomething()
t.complitionHandler?(50)
}
test()
4.2 延時執行逃逸閉包
func makeIncrementer(amount: Int, handler: @escaping (Int) -> Void){
var runningTotal = 0
runningTotal += amount
self.complitionHandler = handler
// 延時執行
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
self.complitionHandler?(50)
}
}
5. 自動閉包
解釋1:
當閉包作為參數傳入時,使用@autoclosure來修飾,就表示如果傳入的是一個普通的值,就會被自動包裝成一個閉包。這個閉包沒有參數,傳回值是String, 也就是我們傳入的普通的字元串。
解釋2:
自動閉包是一種自動建立的閉包,用于包裝傳遞給函數作為參數的表達式。這種閉包不接受任何參數,讓你能夠省略閉包的花括号,用一個普通的表達式來代替顯式的閉包。
并且自動閉包讓你能夠延遲求值,因為直到你調用這個閉包,代碼段才會被執行。要标注一個閉包是自動閉包,需要使用@autoclosure。
例1:
func debugOutPrint(for condition: Bool , _ message: @autoclosure () -> String){
if condition {
print(message())
}
}
func doSomething() -> String{
//do something and get error message
return "NetWork Error Occured"
}
debugOutPrint(for: true, doSomething())
debugOutPrint(for: true, "Application Error Occured")
例2:
// 未使用自動閉包,需要顯示用花括号說明這個參數是一個閉包
func test(closure: () -> Bool) {
}
test(closure: { 1 < 2 } )
// 使用自動閉包,隻需要傳遞表達式
func test(closure: @autoclosure () -> String) {
}
test(customer: 1 < 2)
6.閉包捕獲的變量的記憶體結構
我們通過下面這個例子,來探索閉包捕獲的變量的記憶體結構:
func makeIncrementer() -> () -> Int {
var runningTotal = 12
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
🤔 用下面兩種方式,執行函數,列印結果是什麼?
// 方式一: 直接執行3次函數
print("方式1 - 第1次執行函數:\(makeIncrementer()())")
print("方式1 - 第2次執行函數:\(makeIncrementer()())")
print("方式1 - 第3次執行函數:\(makeIncrementer()())")
print("\n")
// 方法二: 先建立一個函數變量,并執行這個函數變量3次
var makeInc2 = makeIncrementer()
print("方式2 - 第1次執行函數:\(makeInc2())")
print("方式2 - 第2次執行函數:\(makeInc2())")
print("方式2 - 第3次執行函數:\(makeInc2())")
列印結果:
可以看到方式1中,每次執行方法,都是在初始值12的基礎上 + 1. 列印的是13。
方式2中,每次執行方法,都是在上一次執行結果的基礎上 + 1. 列印的是13、14、15。
第一種結果是在我們的預期之中的,因為我們按照平時的邏輯思考,會認為 runningTotal是方法内的局部變量,每一次執行都會重新初始化成12。是以無論執行多少次,應該都是13呀,為什麼方法2中的執行方式卻列印出了非預期的值呢?我們嘗試通過SIL來探索被 block 捕獲的變量的記憶體結構:
被捕獲的變量,會在對上開辟記憶體空間,是以是引用類型。
●方法 2 中, 建立一個函數的變量,每次執行的都是這個函數變量。對于同一個函數變量,它的内部隻有一個 runningTotal的位址,每次執行函數,都會去 runningTotal 的位址上去取值,是以每次拿的都上次 + 1 後的結果。
●方法 1 不能每次都 +1 , 是因為它的本質是建立了3個函數變量,也就對應的在堆上建立了 3個不同的 runningTotal 的記憶體空間,每個 runningTotal 的初始值都是12,是以執行函數,都是 12 + 1 = 13。
7.閉包的記憶體結構
還是上面那個例子,我們探究一個方式2中的,定義的函數變量 makeInc2 的資料類型
// 方法二: 先建立一個函數變量,并執行這個函數變量3次
var makeInc2 = makeIncrementer()
1建立一個makeInc2的變量%3
2執行makeIncrementer函數,并将傳回值存儲到%5中
3将%5的内容存儲給%3
這。。。什麼都看不出啊。。。我們再下沉一層中間代碼,看看 IR 代碼:
IR代碼就非常詳細的展示了整個函數内部的邏輯:
1不止為捕獲變量runningTotal在堆記憶體中建立了記憶體空間,還在後面追加了一個8位元組存放runningTotal的值
2makeIncrementer() 函數的傳回值是這個結構體: { i8*, %swift.refcounted* }
a第一個成員變量是閉包incrementer() 的位址
b第二個成員變量是追加了8位元組的捕獲的外部變量runningTotal位址
我們自己建構一下資料結構:
// makeIncrementer()函數傳回的資料結構
struct FuntionData<T>{
var closuresPointer: UnsafeRawPointer
var captureValuePointer: UnsafePointer<T>
}
// 閉包incrementer()的資料結構
struct Closures<T> {
var refCounted: HeapObject
var value: T
}
// 捕獲的外部變量runningTotal的資料結構
struct HeapObject{
var matedataPointer: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
将makeInc變量,與我們建構的資料結構進行記憶體綁定:
//包裝的結構體
struct VoidIntFun {
var f: () ->Int
}
// 要觀察的函數變量
var makeInc = VoidIntFun(f: makeIncrementer())
// 将變量的位址,綁定到我們自己建構的資料結構
let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
ptr.initialize(to: makeInc)
let ctx = ptr.withMemoryRebound(to: FuntionData<Closures<Int>>.self, capacity: 1) {
$0.pointee
}
// 列印閉包incrementer()
print("閉包的位址:\(ctx.closuresPointer)" )
// 列印捕獲的外部變量runningTotal
print("捕獲的外部變量的HeapObject:\(ctx.captureValuePointer.pointee.heapObject)")
print("捕獲的外部變量的value:\(ctx.captureValuePointer.pointee.value)")
列印結果:
由此,我們可以知道:
●閉包其實是個引用類型,
●閉包的資料結構是閉包位址 + 捕獲的外部變量(外部變量的HeapObject + 外部變量的值)。