讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 2.10 將函數作為值 >

2.10 將函數作為值

如果從未使用過將函數當作一等公民的編程語言,那麼你現在應該坐好了,因為我所說的話可能會讓你昏倒:在Swift中,函數是一等公民。這意味著函數可以用在任何可以使用值的地方。比如,函數可以賦給變量;函數可以作為函數調用的參數;函數可以作為函數的結果返回。

Swift有嚴格的類型。你只能將一個值賦給變量,或是將值傳遞給函數以及從函數傳遞出來,前提是它的類型是正確的。為了將函數當成值來使用,函數需要有一個類型。事實上,函數就是有類型的。能猜出是什麼嗎?函數的簽名就是其類型。

將函數當作值的主要目的在於稍後可以在不知道函數是什麼的情況下調用該函數。

下面是個簡單的示例,只展示了語法與結構:


func doThis(f:->) {
    f
}  

doThis函數接收一個參數,並且無返回值。參數f本身是個函數;因為參數類型不是Int、String或Dog,而是一個函數簽名()->(),這表示一個不接收參數、無返回值的函數。接下來,doThis函數會調用接收到的函數f作為其參數,這正是函數體中參數名後面圓括號的含義。

那麼該如何調用函數doThis呢?你需要傳遞一個函數作為實參。一種方式是將函數名作為參數,如以下代碼所示:


func whatToDo {
    print(\"I did it\")
}
doThis(whatToDo)  

首先,我們聲明了一個恰當類型的函數,該函數不接收參數且無返回值。接下來調用doThis,將函數名作為實參傳遞給它。注意,這裡並沒有調用whatToDo,只是將其傳遞進去。這是因為其名字後面並沒有小括號。當然,這麼做沒問題:將whatToDo作為實參傳遞給doThis;doThis會將接收到的函數作為參數進行調用;然後控制台會打印出\"I did it\"。

不過,這麼做的意義何在?如果目標是調用whatToDo,那為何不直接調用它呢?讓其他函數調用它有什麼好處呢?在剛才給出的示例中看不出來這麼做的好處;我只是介紹一下語法與結構。不過在實際情況下,這麼做是很有價值的,因為其他函數可以以特殊的方式來調用參數函數。比如,可以在完成其他一些事情後再調用它,或稍後再調用。

比如,將函數調用封裝到函數中的一個原因是可以減少重複,降低出錯的可能。如下示例來自於我所編寫的代碼。Cocoa中常做的一件事就是在代碼中直接繪圖,這涉及4個步驟:


let size = CGSizeMake(45,20)
UIGraphicsBeginImageContextWithOptions(size, false, 0) 1
let p = UIBezierPath(
    roundedRect: CGRectMake(0,0,45,20), cornerRadius: 8)
p.stroke 2
let result = UIGraphicsGetImageFromCurrentImageContext 3
UIGraphicsEndImageContext 4  

1打開圖形上下文。

2在上下文中繪製。

3提取圖像。

4關閉圖形上下文。

這麼做醜陋至極。所有代碼的唯一目的就是獲取result,即圖像;不過這個目的散落到了其他代碼中。同時,整個結構是樣本式的;無論在哪個應用中,步驟1、步驟3和步驟4都是一樣的。此外,我很擔心會忘記其中某一步;比如,如果不小心漏掉了步驟4,那麼結果就會非常糟糕。

每次繪製時唯一不同的就是步驟2。因此,步驟2是唯一一個需要編寫的部分!只要編寫一個輔助函數來表示出這個樣板化過程就能解決所有問題:


func imageOfSize(size:CGSize, whatToDraw: -> ) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(size, false, 0)
    whatToDraw
    let result = UIGraphicsGetImageFromCurrentImageContext
    UIGraphicsEndImageContext
    return result
}  

imageOfSize輔助函數很有用,因此我將其聲明在文件頂層,這樣所有文件就都能看到它了。為了繪製圖像,我在函數中執行了步驟2(實際的繪製),然後將該函數作為實參傳遞給imageOfSize輔助函數:


func drawing {
    let p = UIBezierPath(
        roundedRect: CGRectMake(0,0,45,20), cornerRadius: 8)
    p.stroke
}
let image = imageOfSize(CGSizeMake(45,20), drawing)  

這是將繪製指令轉換為圖像的一種漂亮的表示方式。

Cocoa API很多時候都需要傳遞函數,然後由運行時以某種方式或稍後調用。比如,當一個視圖控制器展現視圖時,你所調用的方法會接收3個參數:展現的視圖控制器、表示展現是否要添加動畫的Bool值,以及展現完畢後所調用的函數:


let vc = UIViewController
func whatToDoLater {
    print(\"I finished!\")
}
self.presentViewController(vc, animated:true, completion:whatToDoLater)  

Cocoa文檔常常將這樣的函數描述為處理器,並將其稱作塊,因為這裡需要的是Objective-C語法結構;在Swift中,它是個函數,因此將其當作函數並傳遞就可以了。

有些常見的Cocoa場景甚至會將兩個函數傳遞給一個函數。比如,在執行視圖動畫時,你常常會傳遞兩個函數,一個函數規定動畫動作,另一個函數指定接下來要做的事情:


func whatToAnimate { // self.myButton is a button in the interface
    self.myButton.frame.origin.y += 20
}
func whatToDoLater(finished:Bool) {
    print(\"finished: (finished)\")
}
UIView.animateWithDuration(
    0.4, animations: whatToAnimate, completion: whatToDoLater)  

這表示:改變界面中按鈕的幀原點(即位置),動作持續時間為0.4秒;接下來,當完成時,在控制台中打印出一條日誌消息,說明動畫執行是否完成。

為了讓函數類型說明符更加清晰,請通過Swift的typealias特性創建一個類型別名,為函數類型賦予一個名字。這個名字可以是描述性的,請不要與箭頭運算符符號搞混。比如,如果定義typealias VoidVoidFunction=()->(),那就可以在通過該簽名指定函數類型時使用VoidVoidFunction了。