接續上文的函式,我們來談談「閉包(Closure)」;正如它的名稱一樣,「閉包」這個名詞讓我們這些初學者感覺既抽象又不親切,對於Swift親切易用的形象立馬大打了折扣,但事實上「閉包」是一種更深層次的函式使用方法,不使用閉包,我們一樣可以寫出相同功能的程式,因此這篇您如果沒有興趣,可以先跳過,亦能學習並寫出完整的Swift程式,不過,若我們瞭解並妥善的使用閉包,則可以讓我們的程式更為幹練與精簡。
維基百科對於「閉包」的解釋是這樣的:「閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是參照了自由變數的函式。這個被參照的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。」
看不懂對不對?沒關係,除非真的實作過,否則我們初入門者很難一開始便瞭解它的含義,我找到另外一種比較可以讓人接受的解釋是:「閉包其實也是函式,不過它比較特別的是它擁有一組特殊的環境變數--本來以為應該消失,可是卻依然存在的變數。」,因此,我們大概知道閉包就像一大包的東西,它具有如下的特性:
- 閉包是一種函式
- 這個函式搭配了一組環境變數
- 在使用閉包時,能夠用到這些環境變數
下面我們來透過三個步驟來瞭解「閉包(Closure)」:
- 就像我們可以指定數值或字串給某個變數一樣,「函式」也可以當作參數般,指定給變數使用。
- 當作參數所傳入的函式變數,它可以「Capture」到位於函式上層的變數(也就是,我們可以在閉包函式中取用到外層的變數值)。
- 有兩種方式可以用來建立函式,一種是常見的用func開頭方式宣告,這是最常用的方式,另一種則是用前後大括號{}包起來,這經常用在我們的閉包(Closure)函式中。
A)把函式當作參數值來使用
就像我們經常會指定一個數值或字串給參數一樣,例如下方的範例(請仔細看綠色的說明文字)
// 這是最常見的,宣告一個變數並給予一個數值參數
func printInt(i: Int) {
println("you passed \(i)")
}
//我們也可以把這個函式當作值指定給某個變數
let funVar = printInt
//把printInt指定給變數funVar後,我們就可以像使用原先函式般的來使用funVar,感覺好像是把函式改名了。
funVar(2) // 把funVar當成printInt來使用,會印出"you passed 2"
//所以我們可以有一個函式,其參數是另一個函式,如下紅色的參數所示
func useFunction(funParam: (Int) -> () ) {
//在useFunction中呼叫這個傳入的參數
funParam(3)
}
//所以,我們可以把printInt這個函式當成參數傳入useFunction函式中,
useFunction(printInt)
//也可以把funVar這個函式變數傳入
useFunction(funVar)
B)閉包函式可Capture到上層的變數:
在Closure函式的外層所宣告的變數,其值仍可被該Closure函式所讀取(在Closure我們稱為補獲),請仔細看下方的例子:
//宣告一個returnFunc函式,其回傳值是一個(Int) -> () 的函式
func returnFunc() -> (Int) -> () {
var counter = 0 // counter這個變數待會兒可在下方的innerFunc閉包中被capture
func innerFunc(i: Int) {
//可讀取到counter這個變數,這個變數位在innerFunc閉包外部,這個動作我們稱為capture
counter += i
println("running total is now \(counter)")
}
return innerFunc
}
//下方是這個returnFunc函式使用
let f = returnFunc()
f(3) //會印出"running total is now 3"
f(4) //會印出"running total is now 7"
//如果我們重新呼叫returnFunc,counter的值會重新開始計算
let g = returnFunc()
g(2) //會印出"running total is now 2"
g(2) //會印出"running total is now 4"
//不過,雖然重新呼叫returnFunc,但是counter值並不會相互影響,因為它們屬於不同的變數
f(2) //會印出"running total is now 9"
C)閉包函式的格式:
Swift有兩種函式的宣告方式:
- 用func開頭,這是最常用的方式。
- 用前後大括號{}包起來,常用在Closure函式。
我們看看下方的例子doubler變數,它被塞入了一個用前後大括號{}包起來、未命名(anonymous)的函式作為它的值,這個函式就是閉包Closure。
let doubler = { (i: Int) -> Int in return i*2 }
//這個doubler能像一般的變數般當作參數傳遞
[1, 2, 3].map(doubler)
用前後大括號{}包起來這種方式,不同於用func開頭來宣告,首先,它不需要給名稱,因此它是匿名(anonymous)的。這種函式用法主要用在閉包中,我們稱它是一種自包含(self-contained)的匿名函式:用一個大括號 { } 把程式碼封裝起來,括號裡緊接著函式的型態 ( ) -> ( ) ,型態宣告後面則用 in 這個關鍵字將型態與 Closure 的主程式碼分開,這樣的寫法就是 Closure 最標準的型式;Swift這種closure的方法相當類似於C和Object-C中的程式碼塊、C++和C#中的Lambda運算式、或Java中的匿名內部類別。
因此Swift閉包語法的標準格式如下:
{(參數列表) -> 回傳值型別 in
指令組
}
例:
{( a:Int, b:Int) -> Int in
return a+b
}
我們還可以把它寫成一行如下:
{ (參數列表) -> 回傳值型別 in 指令組 }
例:
{( a:Int, b:Int) -> Int in return a+b }
但Swift其實可以自動根據上下文環境自動判斷出參數型別和回傳值型別,所以我們可以再簡化如下:
{ a, b in return a+b }
這樣的閉包夠簡單了嗎?但還可以更簡化,因為return關鍵字其實也可以不需要:
{ a, b in a+b }
夠簡化了嗎?但如果我們用$0, $1, $2… 來表示閉包中的參數,$0代表第一個參數,$1代表第二個參數,$2代表第三個參數,依此類推則$n+1代表第n個參數,這在Swift中稱為參數名稱縮寫功能,使用此功能後,閉包中的in這個關鍵字也可以省略了,因此我們可以再把上述的閉包寫成如下:
{ $0 + $1 }
很神奇的,本來我們的閉包長得如下:
{( a:Int, b:Int) -> Int in
return a+b
}
竟然可以縮寫成為 { $0 + $1 } ,足見閉包在簡化程式碼、增加可讀性上的便利。
所以,還記得我們一開始舉的doubler例子嗎?它可以逐步簡化如下:
- [1, 2, 3].map( { (i: Int) ->Int in return i * 2 } )
- [1, 2, 3].map( { i in return i * 2 } )
- [1, 2, 3].map( { i in i * 2 } )
- [1, 2, 3].map( { $0 * 2 } )
- [1, 2, 3].map() { $0 * 2 }
- [1, 2, 3].map { $0 * 2 }
D)一個簡單的例子
最後我們再用一個簡單的加減計算函式作為例子,當作本文的總結。
首先我們用標準的方法來實作一次。
// calculate函式會回傳一個函式型別為(Int,Int)-> Int的值
func calculate(opr :String)-> (Int,Int)-> Int {
func add(a:Int, b:Int) -> Int { //定義加法add函式
return a + b
}
func sub(a:Int, b:Int) -> Int { //定義減法sub函式
return a - b
}
var result : (Int, Int)-> Int
switch (opr) { //若傳入為+則傳回add函式,-則傳回sub函式
case “+”:
result = add
case “-“:
result = sub
default:
result = add
}
return result;
}
這個函式的用法是;
var f1 : (Int,Int)-> Int //先宣告一個f1變數,型態為(Int,Int)-> Int的函式
f1 = calculate(“+”) //呼叫calculate函式並傳入+,則會傳回加法add函式儲存於f1變數
println(“10 + 5 = \(f1(10, 5))”) // f1(10, 5)等同於執行add(10, 5)
var f2 : (Int,Int)-> Int //先宣告一個f2變數,型態為(Int,Int)-> Int的函式
f2 = calculate(“-”) //呼叫calculate函式並傳入-,則會傳回加法sub函式儲存於f2變數
println(“10 - 5 = \(f2(10, 5))”) // f1(10, 5)等同於執行sub(10, 5)
接下來我們用閉包的方法來實作一次。
func calculate(opr :String)-> (Int, Int)-> Int {
var result : (Int, Int) -> Int
switch (opr) {
case “+” :
result = {( a: Int, b: Int) -> Int in
return a + b
}
default:
result = {( a: Int, b: Int) -> Int in
return a – b
}
}
return result;
}
簡化閉包格式
請注意紅色的部份,它們分別取代了標準函式作法中的add與sub兩個函式,而這部份就是我們所稱的閉包。依上文中提到的閉包簡化方式,程式碼可改寫如下:
func calculate(opr :String)-> (Int, Int)-> Int {
var result : (Int, Int) -> Int
switch (opr) {
case “+” :
result = { $0 + $1 }
default:
result = { $0 + $1 }
}
return result;
}
謝謝您清楚的講解!
回覆刪除thanks
回覆刪除謝謝,一開始覺得有點困難,但看到後來好像有點了解
回覆刪除清楚易懂
回覆刪除