Swift學習筆記7——閉包(Closures)

其實這個閉包可以看做是匿名的函數。

我們先來回想一下函數作爲參數的情況

//定義一個函數,它最後的參數是一個函數類型
func doMath(first: Int, second: Int, mathFunc: (Int, Int) -> Int) {
    print("mathFunc =",mathFunc(first,second))
}
//定義一個函數,它有兩個整形參數,並有一個整形返回值
func add(first: Int, _ second: Int) -> Int{
    return first + second
}
//調用第一個函數,將第二個函數作爲參數傳入
doMath(1, second: 3, mathFunc: add)
//打印結果爲  mathFunc = 4

如果我們想用doMath實現兩個數相減的方法,那麼必須再寫定義一個sub函數,然後將其作爲參數傳入。這樣在功能多了之後會顯得很麻煩,一堆函數,而所以有了閉包這個概念。

閉包的語法

{ (參數列表) -> 返回類型 in 

      //閉包體

}

有了閉包,我們可以將上面的代碼改爲

//定義一個函數,它最後的參數是一個函數類型
func doMath(first: Int, second: Int, mathFunc: (Int, Int) -> Int) {
    print("mathFunc =",mathFunc(first,second))
}
doMath(1, second: 3, mathFunc: {(f: Int, s: Int) -> Int in
    return f + s
})

還是很麻煩是吧? 別忘了Swift有類型推斷功能,所以我們可以繼續簡化上面的閉包部分代碼

doMath(1, second: 3, mathFunc: {f, s in
    return f + s
})

對應只有一行代碼的閉包,return關鍵字還可以省略

doMath(1, second: 3, mathFunc: {f, s in f + s })

此外,閉包對參數提供了默認名字,依次爲 $0,$1,$2....所以上面的閉包仍可以簡化

doMath(1, second: 3, mathFunc: {$0 + $1 })


對於閉包在參數列表最後一項的情況,可以將閉包寫到小括號外部,並且可以省略掉外部參數名

doMath(1, second: 3){
    var f = $0 + 1
    return f + $1
}

Autoclosures 

姑且叫自動打包吧。用大括號括起來就好,編譯器自動判斷這個大括號裏面的是什麼返回類型。但是有時候不準確,需要自己寫。下面是這個概念的解釋,其實也是一種定義閉包變量的方法。

var t = {
    return 1
}
print(t())


定義了一個Void->Void類型的閉包。因爲沒有參數,所以可以省略參數列表和in關鍵字。如果有參數的話,就不能省略in關鍵字。

var b: Void->Int = {  //定義了一個類型爲 Void->Int的閉包
    var i = 1
    i++
    print(i)
    return i
}

因爲閉包其實就是函數,調用這個閉包就和調用函數一樣。但是有區別的就是閉包都是沒有外部外部參數名,調用的時候不要把內部參數名但做外部參數名使用。


有時候函數需要傳遞一個閉包的時候,可以在調用的時候使用大括號將一段代碼生成爲閉包。

var b: Void->Int = {
    var i = 1
    return i
}
func doClosures(c: Void->Void) {
    c()
}
doClosures({b()})  //雖然b是一個Void->Int的閉包,但是其調用再封裝之後變爲了Void->Void的閉包
doClosures({    
    var i = 3
    i++
    print(i)
})


此外,可以在函數參數列表裏面使用@autoclosure關鍵字,這樣就不用使用大括號封裝了。但是對於多句的代碼情況不行(上面的第二種),有時候自動封裝也會出錯,比如用上面的第一種情況,它把b()看做了Int,然後報錯。需要將返回類型重新定義一下

var b: Void->Void = {
    var i = 1
    i++
    print(i)
//    return i
}
func doClosures(@autoclosure c: Void->Void) {   //或者不改b的類型,將這裏的c的類型改爲 Void->Int也可以
    c()
}
doClosures(b())

如果想要自動封裝的閉包可以在doClosures函數的作用域以外使用,那麼加上escaping關鍵字。這個關鍵字只能用在@autoclosure後面。

var b: Void->Void = {
    var i = 1
    i++
    print(i)
}
var t: (Void->Void)?
func doClosures(@autoclosure(escaping) c: Void->Void) {
    c()
    t = c  //將自動封裝的c賦值給外部變量t
}
doClosures(b())
t!()


閉包的值捕獲

在生成一個閉包的時候,閉包會將它用到的參數和變量都保存一份。提醒一下,其實閉包就是函數。

func giveMeFunc2(step: Int) -> (Void -> Int)? {
    var total = 0
    func add() -> Int { total += step; return total }
    return add
}

上面的函數裏面生成了嵌套函數,通過輸入不同的符號,返回不同的函數。這裏有兩個變量需要注意,一個是total,一個是step。當生成嵌套函數的時候,嵌套函數會將這兩個變量都copy一份,然後保存起來。下面是對上面代碼的一個使用

var f1 = giveMeFunc2(1)! //得到一個函數,它會將傳入的參數累加,並且每次調用都會加上一次step
print("f1=",f1())  // 1
print("f1=",f1())  // 2
var f2 = giveMeFunc2(2)! //得到一個函數,它會將傳入的參數累加,並且每次調用都會減去一次step
print("f2=",f2())  // 2
print("f2=",f2())  // 4
print("f2=",f1())  // 3

可以看到,f1和f2的total和step是不會相互干涉的。

再來看看這個值捕獲的時間,看下面代碼。這裏可以看到,值捕獲是發生在返回之前。這個和OC的block是一樣的。

func giveMeFunc2(step: Int) -> (Void -> Int)? {
    var total = 0
    func add() -> Int { total += step; return total }
    print("before +100",add())  // total = 0
    total += 100
    print("after +100",add())  // total = 100
    return add
}

var f1 = giveMeFunc2(1)! //得到一個函數,它會將傳入的參數累加,並且每次調用都會加上一次step
print("f1=",f1())  // 103
print("f1=",f1())  // 104

看到這裏,可能大家會以爲這個值捕獲和OC的block差不多,但是其實差遠了。這個值捕獲的時間很有區別。這裏明顯的一點就是我們在函數內部改變外部變量total的時候,沒有加任何修飾符,OC裏面必須加上__block,要麼就是對全局變量進行修改。

我們先看一段OC代碼

int t =1;
int(^b)() = ^() { return t; };
t = 3;
NSLog(@"%d",b()); //輸出1,理由就不多說了。
假如我們把t改爲__block。那麼將會輸出3。改爲static同樣的效果。
__block int t =1;
int(^b)() = ^() {  return t;   };
t = 3;
NSLog(@"%d",b());  //3

來看OC和swift中兩段很類似的代碼

//OC
typedef int(^BLOCK)(void);
BLOCK OCFunc (int step) {
    __block int total = 0;
    BLOCK b = ^() { total +=step; return total; };
    step = 100;
    NSLog(@"before +100,%d",b());  //1
    total +=100;
    NSLog(@"after +100,%d",b());   //102
    return b;
}
//在main方法裏面調用
BLOCK b = OCFunc(1);
NSLog(@"%d",b());  // 103
NSLog(@"%d",b());  // 104

//Swift
func swiftFunc(var step: Int) -> Void -> Int{
    var total = 0
    let b: Void -> Int = { Void in total += step; return total }
    step = 100;
    print("before +100,",b())  // 100
    total+=100                 // total = 200
    print("after +100,",b())   //300
    return b
}
let d = swiftFunc(1)
print("d=",d())   //400
print("d=",d())   //500

這裏可以看到,OC中的step在block定義的時候就綁定了,後面在更改step的值也不影響block。但是在swift中,step仍然是可以改變的,直到step離開作用域後,閉包纔將其捕獲。

如果要OC中產生同樣的效果,只需定義一個__block變量,如下。可以這麼看,Swift中的變量默認都是__block的

//OC
typedef int(^BLOCK)(void);
BLOCK OCFunc (int step) {
    __block int total = 0;
    __block int step2 = step;
    BLOCK b = ^() { total +=step2; return total; };
    step2 = 100;
    NSLog(@"before +100,%d",b());  //100
    total +=100;
    NSLog(@"after +100,%d",b());   //300
    return b;
}
//在main方法裏面調用
BLOCK b = OCFunc(1);
NSLog(@"%d",b());  //400
NSLog(@"%d",b());  //500

這個值捕獲和OC的block一樣,也會產生循環引用問題。OC裏面是使用__weak來解決,這裏差不多,它可以在參數列表前面加上捕獲列表,並且對捕獲類別的參數進行權限控制,附上一個官方例子,以後寫ARC的時候詳細講。

lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}



閉包是引用傳遞,意味着將一個閉包賦值給另外一個閉包變量的時候,二者是指向同一個閉包。







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章