Swift 中的閉包

閉包是自包含的函數(shù)代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 CObjective-C 中的代碼塊 (blocks) 以及其他一些編程語言中的匿名函數(shù)比較相似。
閉包可以捕獲和存儲其所在上下文中任意常量和變量的引用,被稱為包裹常量和變量。

閉包表達式

Swift 中可以通過 func 關鍵字 定義一個函數(shù)

func sum(_ v1: Int, _ v2: Int) -> Int {
    return v1 + v2
}

sum 函數(shù)調用:

sum(10, 20)  // 30

也可以用過閉包表達式定義一個函數(shù)

var fn = { (v1: Int, v2: Int) -> Int in
    return v1 + v2
}

閉包表達式調用:

fn(10, 20) // 30

Swift 中函數(shù)是一類公民,可以作為參數(shù)、返回值,跟 Int、ArrayClass 等沒有區(qū)別:

func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}

觀察 exec 函數(shù),這是有三個參數(shù)的無返回值函數(shù):

  • 第1個參數(shù):v1,類型為 Int
  • 第2個參數(shù):v2,類型為 Int
  • 第1個參數(shù):fn,類型為 (Int, Int) -> Int)
  • 無返回值

調用 exec 函數(shù):

exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
    // 注意:這里的 v1 和 v2 是 fn 閉包表達式內(nèi)的參數(shù),跟 exec 函數(shù)的 v1、v2 沒有任何關系
    return v1 + v2
})

閉包的精簡寫法

上面 exec 的調用雖然很容易理解,但看上去有些冗長:參數(shù)類型滿天飛fn 的參數(shù) v1、v2exec本身的前兩個參數(shù)容易混淆。
強大的 Swift 編譯器允許我們對其做一些精簡,下面一步步做介紹:

  • 由于在定義 exec 的時候已經(jīng)明確了 fn 的兩個參數(shù)類型Int和返回類型Int,所以可以做如下簡化:

    // 只需要使用 in 關鍵字 將參數(shù)和函數(shù)體做區(qū)隔即可,省略了v1、v2的類型以及返回值
    exec(v1: 10, v2: 20, fn: { v1, v2 in
        return v1 + v2
    })
    
  • 就跟函數(shù)一樣,閉包函數(shù)體的單行表達式可以省略 return

    // 省略了函數(shù)體的 return 關鍵字
     exec(v1: 10, v2: 20, fn: { v1, v2 in
         v1 + v2
     })
    
  • Swift 中可以使用 $0$1 來分別表示第0個參數(shù)第1個參數(shù)

  exec(v1: 10, v2: 20, fn: {
       $0 + $1
  })
  • 甚至于你可以省略$0$1(這種方式過分了,不推薦??)
  // 直接使用一個 + ,表示第0個參數(shù)和第1個參數(shù)直接是加號運算符
  exec(v1: 10, v2: 20, fn: +)

尾隨閉包

如果你需要將一個很長的閉包表達式作為最后一個參數(shù)傳遞給函數(shù),將這個閉包替換成為尾隨閉包的形式很有用。
尾隨閉包是一個書寫在函數(shù)圓括號之后的閉包表達式,函數(shù)支持將其作為最后一個參數(shù)調用。
在使用尾隨閉包時,你不用寫出它的參數(shù)標簽。

還用之前的 exec 函數(shù)舉例

  • 不使用尾隨閉包:
exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
    return v1 + v2
})
  • 使用尾隨閉包:
    exec(v1: 10, v2: 20) { (v1: Int, v2: Int) -> Int in
       return v1 + v2
    }
    

尾隨閉包不僅僅省略了 fn 形參,而且將{函數(shù)體}挪到了()外面讓整個函數(shù)調用更加的直觀易讀。同樣可以對其進行精簡:
swift // 省略閉包參數(shù)類型和返回值 exec(v1: 10, v2: 20) { v1, v2 in // 省略 return v1 + v2 }
然后:
swift exec(v1: 10, v2: 20) { // 直接使用 $0、$1 表示第0和第1個參數(shù) $0 + $1 }
這個表達式就非常的簡潔優(yōu)雅了!???? 注意,下面的表達式是不允許的:
swift // 尾隨閉包不允許省略$0、$1 exec(v1: 10, v2: 20) { + }

閉包的值捕獲

閉包可以理解為函數(shù)以及其捕獲的上下文中的變量或常量的總和

看下面這個函數(shù):

func getFn() -> (Int) -> Int {
    var num = 0
    func plus (_ i: Int) -> Int {
        num = num + i
        return num
    }
    return plus
}

getFn函數(shù)沒有參數(shù),返回值為(Int) -> Int(一個參數(shù)為Int返回值為Int的函數(shù))。

getFn函數(shù)體內(nèi)定義了一個Int類型的變量num,又定義了一個plus函數(shù),并將其作為getFn函數(shù)的返回值返回。

plus函數(shù)對num變量進行了捕獲,構成了閉包。

思考如下代碼的輸出:

var f = getFn()
print(f(1))
print(f(2))
print(f(3))
print(f(4))

結果是哪一組?

1         1
2   or    3
3         6
4         10

正如前面提到的函數(shù)以及其捕獲的上下文中的變量或常量的總和,當調用getFn()時,返回的不僅僅是plus函數(shù)同時也包括num變量組成的閉包整體!理解這個概念非常重要,因此getFn()返回的其實就是下面的代碼片段:

var num = 0
func plus (_ i: Int) -> Int {
    num = num + i
    return num
}

因此f(1)``f(2)``f(3)``f(4)訪問的是同一個num,或者說同一塊變量內(nèi)存

num 初始值為 0
f(1)就等價于 num = num + 1
f(2)就等價于 num = num + 2
f(3)就等價于 num = num + 3
f(4)就等價于 num = num + 4

所以結果為:

1
3
6
10

再思考這種情況:

var f1 = getFn()
print(f1(1))   // 1
print(f1(2))   // 3
print(f1(3))   // 6

var f2 = getFn()
print(f2(1))   // 1
print(f2(2))   // 3
print(f2(3))   // 6

很顯然每創(chuàng)建一個getFn函數(shù)引用(沒錯,函數(shù)和閉包都是引用類型),Swift 都會為所捕獲的num申請一份新的堆空間內(nèi)存,來保證所有的f1訪問的都是同一塊內(nèi)存地址,所有的f2訪問的也都是同一塊內(nèi)存地址,但f1f2訪問的num堆地址不是同一塊!

自動閉包

觀察下面這個函數(shù):

// 如果第1個參數(shù)大于0則返回之,否則返回第2個參數(shù)
func getFirstPositiveNumber(n1: Int, n2: Int) -> Int {
    return n1 > 0 ? n1 : n2
}

調用getFirstPositiveNumber

func getDoubleOfNumber(_ v: Int) -> Int {
    return v * 2
}
getFirstPositiveNumber(n1: 10, n2: 20)  // 10
getFirstPositiveNumber(n1: -10, n2: 20) // 20

上述函數(shù)看似很簡單,但有一個隱患可以優(yōu)化:

如果n1 > 0,那么n2是什么根本不重要了,可是編譯器還是需要花費開銷去"關心"n2。你可能會不以為然,心理嘀咕『不就一個Int,至于么?"』

那下面這個例子呢:

func getNumber() -> Double {
    return Double.pi * 10.0
}
getFirstPositiveNumber(n1: 10.0, n2: getNumber())  // 10.0

既然n1已經(jīng)>0了,我們?yōu)楹芜€要去調用getDoubleOfNumber來計算n2呢?

如果getDoubleOfNumber函數(shù) 計算很復雜需要去讀取本地數(shù)據(jù) 甚至 需要聯(lián)網(wǎng)抓取數(shù)據(jù) 呢?這種浪費就不能不以為然了吧。

那怎么解決呢?當時是使用閉包:

func getFirstPositiveNumber(n1: Double, n2: () -> Double) -> Double {
    return n1 > 0 ? n1 : n2()
}

n2的類型從Double改為() -> Double,調用時:

getFirstPositiveNumber(n1: 10.0, n2: {
    Double.pi * 10.00
})

或者使用尾隨閉包:

getFirstPositiveNumber(n1: 10) {
    Double.pi * 10.00
}

n1 > 0時,閉包的函數(shù)體Double.pi * 10.00根本不用執(zhí)行!完美!

可是每次調用getFirstPositiveNumber都要寫閉包會很繁瑣,因此Swift標準庫提供了自動閉包的語法糖來解決這個問題,getFirstPositiveNumber函數(shù)只需要像下面這么寫:

func getFirstPositiveNumber1(n1: Double, n2: @autoclosure () -> Double) -> Double {
    return n1 > 0 ? n1 : n2()
}
getFirstPositiveNumber1(n1: 10, n2: Double.pi * 2)

調用時n2會自動寫成閉包的形式!

逃逸閉包

func fn1(_ closure: (Int) -> Int) {
    print(closure(10))
}

fn1函數(shù)只有一個閉包參數(shù)closure,且closurefn1函數(shù)體內(nèi)部直接調用,這時候我們稱closure為非逃逸閉包。

如果像下面這么寫編譯器就會報錯:

var c: ((Int) -> Int)?
func fn2(_ closure: (Int) -> Int) {
    c = closure
}

closurefn2作用域外調用,即成為逃逸閉包,可以使用@escaping關鍵詞消除編譯器報錯:

func fn2(_ closure: @escaping (Int) -> Int) {
    c = closure
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容