2015年2月5日 星期四

Swift學習心得 – Closure

    接續上文的函式,我們來談談「閉包(Closure)」;正如它的名稱一樣,「閉包」這個名詞讓我們這些初學者感覺既抽象又不親切,對於Swift親切易用的形象立馬大打了折扣,但事實上「閉包」是一種更深層次的函式使用方法,不使用閉包,我們一樣可以寫出相同功能的程式,因此這篇您如果沒有興趣,可以先跳過,亦能學習並寫出完整的Swift程式,不過,若我們瞭解並妥善的使用閉包,則可以讓我們的程式更為幹練與精簡。   
維基百科對於「閉包」的解釋是這樣的:「閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是參照了自由變數的函式。這個被參照的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。
看不懂對不對?沒關係,除非真的實作過,否則我們初入門者很難一開始便瞭解它的含義,我找到另外一種比較可以讓人接受的解釋是:「閉包其實也是函式,不過它比較特別的是它擁有一組特殊的環境變數--本來以為應該消失,可是卻依然存在的變數。」,因此,我們大概知道閉包就像一大包的東西,它具有如下的特性:
  • 閉包是一種函式
  • 這個函式搭配了一組環境變數
  • 在使用閉包時,能夠用到這些環境變數
下面我們來透過三個步驟來瞭解「閉包(Closure)」:
  1. 就像我們可以指定數值或字串給某個變數一樣,「函式」也可以當作參數般,指定給變數使用。
  2. 當作參數所傳入的函式變數,它可以「Capture」到位於函式上層的變數(也就是,我們可以在閉包函式中取用到外層的變數值)。
  3. 有兩種方式可以用來建立函式,一種是常見的用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有兩種函式的宣告方式:
  1. 用func開頭,這是最常用的方式。
  2. 用前後大括號{}包起來,常用在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. [1, 2, 3].map( { (i: Int) ->Int in return i * 2 } )
  2. [1, 2, 3].map( { i in return i * 2 } )
  3. [1, 2, 3].map( { i in i * 2 } )
  4. [1, 2, 3].map( { $0 * 2 } )
  5. [1, 2, 3].map() { $0 * 2 }
  6. [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;
}

4 則留言: