天天看點

Swift閉包簡要概述

1.閉包

閉包是一個捕獲了外部變量或者常量的函數,可以有名字的函數,可以是匿名的函數,也可以是不捕獲外部變量的函數。是以可以說閉包是特殊的函數。

閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 ObjC 中的代碼塊(blocks)比較相似。

捕獲的變量,可以寫在捕獲清單裡. 如果使用捕獲清單,即使省略了參數名字、參數類型、傳回類型,也必須要用 in 的關鍵字

●捕獲清單裡面的是捕獲的是值,不可變,

●未在捕獲清單裡,捕獲到的是位址,可以修改

案例1: 閉包内部不能修改值傳遞的

Swift閉包簡要概述

案例2: 閉包外部修改值傳遞的資料,也是無效的

Swift閉包簡要概述

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())")      

列印結果:

Swift閉包簡要概述

可以看到方式1中,每次執行方法,都是在初始值12的基礎上 + 1. 列印的是13。

方式2中,每次執行方法,都是在上一次執行結果的基礎上 + 1. 列印的是13、14、15。

第一種結果是在我們的預期之中的,因為我們按照平時的邏輯思考,會認為 runningTotal是方法内的局部變量,每一次執行都會重新初始化成12。是以無論執行多少次,應該都是13呀,為什麼方法2中的執行方式卻列印出了非預期的值呢?我們嘗試通過SIL來探索被 block 捕獲的變量的記憶體結構:

Swift閉包簡要概述

被捕獲的變量,會在對上開辟記憶體空間,是以是引用類型。

●方法 2 中, 建立一個函數的變量,每次執行的都是這個函數變量。對于同一個函數變量,它的内部隻有一個 runningTotal的位址,每次執行函數,都會去 runningTotal 的位址上去取值,是以每次拿的都上次 + 1 後的結果。

●方法 1 不能每次都 +1 , 是因為它的本質是建立了3個函數變量,也就對應的在堆上建立了 3個不同的 runningTotal 的記憶體空間,每個 runningTotal 的初始值都是12,是以執行函數,都是 12 + 1 = 13。

7.閉包的記憶體結構

還是上面那個例子,我們探究一個方式2中的,定義的函數變量 makeInc2 的資料類型

// 方法二: 先建立一個函數變量,并執行這個函數變量3次
var makeInc2 = makeIncrementer()      
Swift閉包簡要概述

1建立一個makeInc2的變量%3

2執行makeIncrementer函數,并将傳回值存儲到%5中

3将%5的内容存儲給%3

這。。。什麼都看不出啊。。。我們再下沉一層中間代碼,看看 IR 代碼:

Swift閉包簡要概述

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)")      

列印結果:

Swift閉包簡要概述

由此,我們可以知道:

●閉包其實是個引用類型,

●閉包的資料結構是閉包位址 + 捕獲的外部變量(外部變量的HeapObject + 外部變量的值)。

繼續閱讀