從零學(xué)習(xí)Swift 05:閉包表達(dá)式和閉包

一:閉包表達(dá)式的定義

在 Swfit 中,可以通過func定義一個(gè)函數(shù),也可以通過閉包表達(dá)式定義一個(gè)函數(shù).閉包表達(dá)式的格式為:

{
(參數(shù)列表) -> 返回值類型 in
方法體
}

比如說我們使用func定義一個(gè)add方法:


func add(a: Int, b: Int) -> Int{
    a + b
}

print(add(a: 10, b: 20))

用閉包表達(dá)式也可以定義:


let fn = {
    (a: Int , b: Int) -> Int in
    a + b
}

print(fn(10,20))

二:閉包表達(dá)式的簡寫

定義一個(gè)函數(shù)如下:

// 接受兩個(gè) Int 類型的參數(shù) , 和一個(gè)函數(shù)類型的參數(shù)
func exec(a: Int, b: Int, fn: (Int, Int) -> Int){
    print(fn(a,b))
}

使用閉包表達(dá)式完整調(diào)用如下:

exec(a: 10, b: 20, fn: {
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
})

另外還有4中簡寫方式:

  1. 省略參數(shù)類型 和 返回值類型
    因?yàn)殚]包表達(dá)式已經(jīng)明確定義了參數(shù)類型和返回值類型都是 Int 類型,所以在調(diào)用閉包表達(dá)式的時(shí)候,編譯器能自動(dòng)類型推斷,知道具體的類型.所以類型可以省略
exec(a: 10, b: 20) {
    v1, v2 in
    return v1 + v2
}
  1. 省略 return
    因?yàn)楹瘮?shù)體代碼是一個(gè)單一表達(dá)式,所以我們可以省略 return
exec(a: 10, b: 20) {
    v1, v2 in v1 + v2
}
  1. 省略參數(shù),$0 $1分別代表第一個(gè),第二個(gè)參數(shù)
exec(a: 10, b: 20) {
    $0 + $1
}
  1. 直接 +
    如果直接寫一個(gè)運(yùn)算符,編譯器也知道是拿兩個(gè)參數(shù)直接參與運(yùn)算
exec(a: 10, b: 20, fn: +)
三:尾隨閉包

如果一個(gè)函數(shù)的最后一個(gè)實(shí)參是一個(gè)閉包表達(dá)式,并且這個(gè)閉包表達(dá)式的函數(shù)體代碼很長,為了增強(qiáng)函數(shù)的可讀性,這個(gè)閉包表達(dá)式可以采用尾隨閉包.

尾隨閉包:尾隨閉包是一個(gè)書寫在函數(shù)調(diào)用括號外面的閉包表達(dá)式.

像上面的exec函數(shù),它的最后一個(gè)參數(shù)是一個(gè)閉包表達(dá)式,那我們我們可以使用尾隨閉包來增強(qiáng)可讀性:(事實(shí)上,編譯器自動(dòng)敲出來的就是尾隨閉包)

func exec(a: Int, b: Int, fn: (Int, Int) -> Int){
    print(fn(a,b))
}

// 尾隨閉包
exec(a: 10, b: 20) { (v1, v2) -> Int in
    return v1 + v2
}

如果函數(shù)只有一個(gè)參數(shù),而且這個(gè)參數(shù)是個(gè)閉包表達(dá)式,并且使用了尾隨閉包的寫法,那么在調(diào)用的時(shí)候可以省略小括號():

func add(fn: (Int, Int) -> Int){
    print(fn(20,10))
}

//正常寫法
add(fn: {$0 - $1})

//尾隨閉包寫法
add(){$0 - $1}

//省略小括號()
add{$0 - $1}

示例:
Swift 中的Array有一個(gè)排序方法:

func sort(by areInIncreasingOrder: (Self.Element, Self.Element) throws -> Bool)

接收兩個(gè)參數(shù),返回一個(gè)布爾值.假設(shè)第一個(gè)參數(shù)是v1,第二個(gè)參數(shù)是v2,它的意思是:如果返回true,v1排在v2前面;如果返回false,v1排在v2后面.

常規(guī)寫法是傳入一個(gè)函數(shù):

func arraySort(){
    //排序規(guī)則函數(shù)
    func comp(v1: Int, v2: Int) -> Bool{
        //從大到小排序
        v1 > v2
    }
    
    var array = [4,2,10,19,13,8,22]
    //傳入 comp 函數(shù)
    array.sort(by: comp(v1:v2:))
    print(array)
}

也可以使用尾隨閉包:

//閉包表達(dá)式示例  數(shù)組排序
func arraySort(){
    var array = [4,2,10,19,13,8,22]
    
    //閉包表達(dá)式常規(guī)寫法
    array.sort(by: {
        (v1: Int, v2: Int) -> Bool in
        return v1 > v2
    })
    
    //因?yàn)槭亲詈笠粋€(gè)參數(shù),可以采用尾隨閉包寫法
    array.sort(){
        v1,v2 in v1 > v2
    }
    
    //因?yàn)槭亲詈笠粋€(gè)參數(shù),并且是唯一一個(gè)參數(shù),可以省略小括號
    array.sort{
        v1,v2 in v1 > v2
    }
    
    //使用 $ 替代參數(shù)
    array.sort{
        $0 > $1
    }
    
    //直接使用 >
    array.sort(by: >)
    
    print(array)
}

四:閉包

閉包和閉包表達(dá)式完全是兩個(gè)概念:

  1. 閉包表達(dá)式: 閉包表達(dá)式是定義函數(shù)的一種方式;
  2. 閉包: 閉包是一個(gè)函數(shù)和它所捕獲的變量/常量環(huán)境組合起來,稱作閉包.
    一般指定義在函數(shù)內(nèi)部的函數(shù)
    一般它捕獲的是外部函數(shù)的局部變量/常量

思考一下以下代碼的打印結(jié)果是什么:

typealias Fn = (Int) -> Int
func getFn() -> Fn{
    
    var num = 0
    
    func sum(_ v1: Int) -> Int{
        num += v1
        return num
    }
    
    return sum
}

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

結(jié)果如下:


怎么會(huì)這樣呢?num是在getFn函數(shù)內(nèi)部定義的局部變量,在 149 行調(diào)用完getFn()后,num的內(nèi)存應(yīng)該被回收了,為什么打印的結(jié)果都是在同一塊內(nèi)存上累加的呢?

其實(shí)這就是閉包,sum 函數(shù)捕獲了 num 變量,并且把 num 變量存在了堆空間,所以即使函數(shù)調(diào)用完畢, num 變量依然沒有銷毀.

我們來看一下它的匯編代碼:

向堆空間申請內(nèi)存

然后我們再修改一下sum函數(shù)的代碼,使其不訪問外部變量,再看看它的匯編,對比一下:

sum 函數(shù)不訪問外部變量

匯編如下:

可以看到,當(dāng)sum函數(shù)沒有訪問外部變量的時(shí)候,它的匯編代碼很簡單.也沒有調(diào)用allocObject向堆空間申請內(nèi)存.并且getFn返回的直接就是sum函數(shù)的地址.

現(xiàn)在,我們大概也知道了,當(dāng) sum 函數(shù)訪問了外層函數(shù)的局部變量 num 時(shí),會(huì)調(diào)用 allocObject 函數(shù)向堆空間申請內(nèi)存,把 num 變量存儲(chǔ)在堆空間,保住 num 變量的命.也就是所謂的 捕獲.

下面我們將通過匯編代碼驗(yàn)證我們的結(jié)論.

如果所示,打兩個(gè)斷點(diǎn):

斷點(diǎn)

運(yùn)行代碼:


getFn 函數(shù)返回值

上面沒有訪問外部變量時(shí),getFn直接返回的就是sum函數(shù)的地址.但是這里不一樣了,因?yàn)樵L問了外部變量,getFn返回的是一個(gè)堆空間的地址.這個(gè)地址中有可以找到sum函數(shù)地址的線索,并且還要存儲(chǔ)捕獲的num變量.

繼續(xù)跳到下一個(gè)斷點(diǎn):

執(zhí)行fn(1)

執(zhí)行完fn(1),內(nèi)存中的值變成了 1.

繼續(xù)跳過斷點(diǎn),會(huì)執(zhí)行fn(2):

執(zhí)行fn(2

執(zhí)行fn(3):

執(zhí)行fn(3)

執(zhí)行fn(4):

執(zhí)行fn(4)

通過上面的分析,已經(jīng)很清晰的知道了當(dāng)函數(shù)訪問了外部函數(shù)的局部變量后,會(huì)向堆空間申請內(nèi)存,用來捕獲外部函數(shù)的變量.

思考一下,調(diào)用兩次getFn()下面代碼打印結(jié)果是什么?

調(diào)用兩次 getFn()

我們看看它的匯編代碼:

調(diào)用兩次getFn()

返回了兩個(gè)Fn對象:

執(zhí)行 fn1(1):

執(zhí)行 fn1(1)

執(zhí)行 fn2(2):

執(zhí)行 fn2(2)

執(zhí)行 fn1(3):

執(zhí)行 fn1(3)

執(zhí)行 fn2(4):

執(zhí)行 fn2(4

可以看到,執(zhí)行多少次getFn(),就會(huì)創(chuàng)建多少個(gè)Fn對象,并且每個(gè)對象之間互不影響.他們都有自己的內(nèi)存空間,都有自己的捕獲對象.

上面的fn1 , fn2和它捕獲的變量組合在一起就是一個(gè)閉包,其實(shí)閉包和類的實(shí)例對象很相似,我們通過兩張圖對比一下:

閉包 和 實(shí)例對象對比

另外他們都存儲(chǔ)在堆空間,并且內(nèi)存布局也一樣,所以我們可以把閉包想象成一個(gè)實(shí)例對象,這樣更容易理解.

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

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

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