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
}
閉包是引用傳遞,意味著將一個閉包賦值給另外一個閉包變數的時候,二者是指向同一個閉包。