Swift進階雜談7:閉包

語言: CN / TW / HK

什麼是閉包

閉包是一個捕獲了上下文的常量或者變數的函式。

func test(){
    print("test") 
}
複製程式碼

上面的函式是一個全域性函式,也是一種特殊的閉包,只不過當前的全域性函式並不捕獲值。

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    print("----")
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
複製程式碼

上面的incrementer我們稱之為內嵌函式,同時從上層函式makeIncrementer中捕獲變數runnungTotal

let closure: (Int) -> Int

closure = {(age: Int) in
    return age
}
複製程式碼

這種就屬於我們比較熟知的閉包表示式,是一個匿名函式,而且從上下文中捕獲變數和常量。 其中閉包表示式是Swift語法。使用閉包表示式能更簡潔的傳達資訊。當然閉包表示式的好處有很多:

  • 利用上下文推斷引數和返回值型別
  • 單表示式可以隱式返回,即省略return關鍵字
  • 引數名稱的簡寫(比如我們的$0)
  • 尾隨閉包表示式

閉包表示式

我們先來一起回顧一下閉包表示式的定義。

{ (param) -> ReturnType  in
    //方法體 to something
}
複製程式碼

首先按照我們之前的知識積累,OC中的Block其實是一個匿名函式,所以這個表示式要具備

  • 作用域(也就是大括號)
  • 函式和返回值
  • 函式題(in)之後的程式碼

Swift中的閉包既可以當做變數,也可以當做引數傳遞,這裡我們看一下下面的例子熟悉一下:

let closure: (Int) -> Int

closure = {(age: Int) in
    return age
}
複製程式碼

同樣的我們也可以把我們的閉包宣告一個可選型別:

 //錯誤的寫法
  var closure : (Int) -> Int?
  closure = nil
 //正確的寫法
  var closure : ((Int) -> Int)? 
  closure = nil
複製程式碼

還可以通過let關鍵字將閉包宣告為一個常量(也就意味著一旦賦值之後就不能改變了)

 let closure: (Int) -> Int
closure = {(age: Int) in
    return age
}
//再次賦值會報錯 改成var宣告就不會了
 closure = {(age: Int) in
    return age
}
複製程式碼

同時也可以作為函式的引數

func test(param : () -> Int){
    print(param())
}

var age = 10

test { () -> Int in
    age += 1
    return age
}
複製程式碼

尾隨閉包

當我們把閉包表示式作為函式的最後一個引數,如果當前的閉包表示式很長,我們可以通過尾隨閉包的書寫方式來提高程式碼的可讀性。

func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool{
   return  by(a, b, c)
}

test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
    
    return (item1 + item2 < item3)
})


複製程式碼

如果上面的引數再長一點,這裡我們看一個函式呼叫是不是就非常費勁,特別是在程式碼量多的時候

test(10, 20, 30){(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
   return (item1 + item2 < item3)
}
複製程式碼

這樣一眼看上去就知道一個函式呼叫,後面是一個閉包表示式。大家看這個寫法,當前閉包表示式{}放在了函式外面 其實如下array.sorted其實就是一個尾隨閉包,而且這個函式就只有一個引數。可以逐漸簡化

var array = [1, 2, 3]

array.sort{(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 })

array.sort{(item1, item2) in item1 < item2 }

array.sort{ return $0 < $1 } //self

array.sort{ $0 < $1 }

array.sort(by: <)
複製程式碼

捕獲值

這裡我們藉助官方文件中的例子來具體說明:

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = makeIncrementer()

print(makeInc())
print(makeInc())
print(makeInc())
複製程式碼

可以思考下,上面的print輸出的都是什麼? 這裡每次都會在上次函式執行的基礎上累加。按道理來說runningTotal是一個臨時變數,每次進來的時候應該是10,這裡每次卻會累加,所以我們通過SIL來看一看發生了什麼? 我們在SIL的文件中來搜尋一下: 這裡我們也可以通過斷點來看一下,確實呼叫了swift_allocObject這個方法。 總結:

  • 一個閉包能夠從上下文捕獲已被定義的常量和變數。即使定義這些常量和變數的原作用域已經不存在,閉包仍能夠在其函式體內引用和修改這些值。
  • 當我們每次修改的捕獲值的時候,修改的是堆區的value
  • 當每次重新執行當前函式的時候,都會重新建立記憶體空間、

逃逸閉包

逃逸閉包的定義:當閉包作為一個實際引數傳遞給一個函式的時候,並且在函式返回之後呼叫,我們就說明這個閉包逃逸了。當我們宣告一個接受閉包作為形式引數時,你可以在形式引數前寫@escaping來明確閉包是允許逃逸的。 Swift3.0之後,系統預設閉包引數是被@noescaping,這裡我們可以通過SIL看出來 如果我們用@escaping修飾閉包之後,我們必須顯示的閉包中使用self。我們來看第一種情況:

class LGTeacher{

    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) {
            print($0)
        }
    }

    deinit {
        print("LGTeaher deinit")
    }

}

var t = LGTeacher()

t.doSomething()
複製程式碼

當前我們的complitionHandler作為當前LGTeacher是在當前方法makeIncrementer呼叫完成之後才會呼叫,這個時候閉包的生命週期是要比當前方法的生命週期長,所以我們說complitionHandler這個閉包逃逸了。 我們再接著看另外一個例子:

class LGTeacher{

    var complitionHandler: ((Int)->Void)?

    func makeIncrementer(amount: Int,  handler: @escaping (Int) -> Void){
        var runningTotal = 0
        runningTotal += amount

//        self.complitionHandler = handler
        DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) {
            handler(runningTotal)
        }
        
        print("makeIncrementer")
        
    }

    func doSomething(){
        self.makeIncrementer(amount: 10) {
            print($0)
            
        }
        print("doSomething")
    }

    deinit {
        print("LGTeaher deinit")
    }

}

var t = LGTeacher()

t.doSomething()

t.complitionHandler?(10)
複製程式碼

這裡的例子同樣的,當前方法執行的過程中不會等待閉包執行完成之後再執行,而是直接返回,所以當前閉包的生命週期比方法長,這裡我們要將閉包宣告成逃逸閉包的方式。

自動閉包

我們先來看下面這個例子

func debugOutPrint(_ condition: Bool , _ message: String){ 
    if condition {
        print("lg_debug:\(message)")
    }
}
 debugOutPrint(true, "Application Error Occured")
複製程式碼

上面程式碼會在當前condition為true的時候,列印我們當前的錯誤資訊,也就意味著false的時候當前條件不會執行。 如果我們當前的字串可能是在某個業務邏輯功能中獲取的,比如瞎main這樣:

func debugOutPrint(_ condition: Bool , _ message: String){ 
    if condition {
        print("lg_debug:\(message)")
    } 
}

func doSomething() -> String{
    //do something and get error message 
    return "NetWork Error Occured"
 }

 debugOutPrint(true, doSomething())

複製程式碼

這時候我們發現一個問題,那就是當前的condition無論是true還是false,當前的doSomething方法都會執行。如果當前的doSomething是一個耗時的任務操作,那麼這裡是不是就造成了一定的資源浪費。 這個時候我們想到的是把當前的引數修改成一個閉包,

func debugOutPrint(for condition: Bool , _ message: () -> String){
    if condition {
        print(message())
    }
}

func doSomething() -> String{
    //do something and get error message
    print("doSomething")
    return "NetWork Error Occured"
}

debugOutPrint(for: false, doSomething())
複製程式碼

這樣的活是不是就能夠正常在當前條件滿足的時候呼叫我們當前的doSomething的方法啊。同樣的問題又隨之而來,那就是這裡是一個閉包,如果我們這個時候就是傳入一個String怎麼辦呢?

func debugOutPrint(for condition: Bool , _ message: @autoclosure () -> String){
    if condition {
        print(message())
    }
}

func doSomething() -> String{
    //do something and get error message
    print("doSomething")
    return "NetWork Error Occured"
}

debugOutPrint(for: false, doSomething())

debugOutPrint(for: true, "Application Error Occured")
複製程式碼

上面我們使用@autoclosure將當前的表示式宣告成了一個自動閉包,不接收任何引數,返回值是當前內部表示式的值。所以實際上我們傳入的String就是放入到一個閉包表示式中,在呼叫的時候返回。