1. 程式人生 > >Swift學習筆記7——閉包(Closures)

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
}


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