- 原文地址:Functional Programming for Android Developers — Part 3
- 原文作者:Anup Cowkur
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:miguoer
- 校對者:shi-xiaopeng Cielsk
在上一章,我們學習了不可變性和併發。在這一章,我們將學習高階函式和閉包。
如果你還沒有閱讀過第一部分和第二部分,可以點選這裡閱讀:
高階函式
高階函式是可以接受將函式作為輸入引數,也可以接受將函式作為輸出結果的一類函式。很酷吧?
但是為什麼有人想要那樣做呢?
讓我們看一個例子。假設我想壓縮一堆檔案。我想用兩種壓縮格式來做 — ZIP 或者 RAR 格式。如果用傳統的 Java 來實現,通常會使用 策略模式。
首先,建立一個定義策略的介面:
public interface CompressionStrategy {
void compress(List<File> files);
}
複製程式碼
然後,像以下程式碼一樣實現兩種策略:
public class ZipCompressionStrategy implements CompressionStrategy {
@Override public void compress(List<File> files) {
// Do ZIP stuff
}
}
public class RarCompressionStrategy implements CompressionStrategy {
@Override public void compress(List<File> files) {
// Do RAR stuff
}
}
複製程式碼
在執行時,我們就可以使用任意一種策略:
public CompressionStrategy decideStrategy(Strategy strategy) {
switch (strategy) {
case ZIP:
return new ZipCompressionStrategy();
case RAR:
return new RarCompressionStrategy();
}
}
複製程式碼
使用這種方式有一堆的程式碼和需要遵循的格式。
其實我們所要做的只是根據不同的變數實現兩種不同的業務邏輯。由於業務邏輯不能在 Java 中獨立存在,所以必須用類和介面去修飾。
如果能夠直接傳遞業務邏輯,那不是很好嗎?也就是說,如果可以把函式當作變數來處理,那麼能否像傳遞變數和資料一樣輕鬆地傳遞業務邏輯?
這正是高階函式的功能!
現在,從高階函式的角度來看這同一個例子。這裡我要使用 Kotlin ,因為 Java 8 的 lambdas 表示式仍然包含了我們想要避免的 一些建立函式介面的方式 。
fun compress(files: List<File>, applyStrategy: (List<File>) -> CompressedFiles){
applyStrategy(files)
}
複製程式碼
compress
方法接受兩個引數 —— 一個檔案列表和一個型別為 List<File> -> CompressedFiles
的 applyStrategy
函式。也就是說,它是一個函式,它接受一個檔案列表並返回 CompressedFiles
。
現在,我們呼叫 compress
時,傳入的引數可以是任意接收檔案列表並返回壓縮檔案的函式。:
compress(fileList, {files -> // ZIP it})
compress(fileList, {files -> // RAR it})
複製程式碼
這樣程式碼看起來乾淨多了。
所以高階函式允許我們傳遞邏輯並將程式碼當作資料處理。
閉包
閉包是可以捕捉其環境的函式。讓我們通過一個例子來理解這個概念。假設給一個 view 設定了一個 click listener,在其方法內部想要列印一些值:
int x = 5;
view.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
System.out.println(x);
}
});
複製程式碼
Java 裡面不允許我們這樣做,因為 x
不是 final 的。在 Java 裡 x
必須宣告為 final,由於 click listener
可能在任意時間執行, 當它執行時 x
可能已經不存在或者值已經被改變,所以在 Java 裡 x
必須宣告為 final
。Java 強制我們把這個變數宣告為 final,實際上是為了把它設定成不可變的。
一旦它是不可變的,Java 就知道不管 click listener 什麼時候執行,x
都等於 5
。這樣的系統並不完美,因為 x
可以指向一個列表,儘管列表的引用是不可變的,其中的值卻可以被修改.
Java 沒有一個機制可以讓函式去捕捉和響應超過它作用域的變數。Java 函式不能捕捉或者涵蓋到它們環境的變化。
讓我們嘗試在 Kotlin 中做相同的事。我們甚至不需要匿名內部類,因為在 Kotlin 中函式是「一等公民」:
var x = 5
view.setOnClickListener { println(x) }
複製程式碼
這在 Kotlin 中是完全有效的。Kotlin 中的函式都是閉包。他們可以跟蹤和響應其環境中的更新。
第一次觸發 click listener 時, 會列印 5
。如果我們改變 x
的值比如令 x = 9
,再次觸發 click listener ,這次會列印9
。
我們能利用閉包做什麼?
閉包有很多非常好的用例。無論何時,只要你想讓業務邏輯響應環境中的狀態變化,那就可以使用閉包。
假設你在一個按鈕上設定了點選 listener, 點選按鈕會彈出對話方塊向使用者顯示一組訊息。如果沒有閉包,則每次訊息更改時都必須使用新的訊息列表並且初始化新的 listener。
有了閉包,你可以在某個地方儲存訊息列表並把列表的引用傳遞給 listener,就像我們上面做的一樣,這個 listener 就會一直展示最新的訊息。
**閉包也可以用來徹底替換物件。**這種用法經常出現在函數語言程式設計語言的程式設計實踐中,在那裡你可能需要用到一些 OOP(物件導向程式設計)的程式設計方法,但是所使用的語言並不支援。
我們來看個例子:
class Dog {
private var weight: Int = 10
fun eat(food: Int) {
weight += food
}
fun workout(intensity: Int) {
weight -= intensity
}
}
複製程式碼
我有一條狗在餵食時體重增加,運動時體重減輕。我們能用閉包來描述相同的行為嗎?
fun main(args: Array<String>) {
dog(Action.feed)(5)
}
val dog = { action: Action ->
var weight: Int = 10
when (action) {
Action.feed -> { food: Int -> weight += food; println(weight) }
Action.workout -> { intensity: Int -> weight -= intensity; println(weight) }
}
}
enum class Action {
feed, workout
}
複製程式碼
dog
函式接受一個 Action
引數,這個 action 要麼是給狗餵食,要麼是讓它去運動。當在 main
中呼叫 dog(Action.feed)(5)
,結果將是 15
。 dog
函式接受了一個 feed
動作,並返回了另外一個真正去給狗餵食的函式。如果把 5
傳遞給這個返回的函式,它將把狗狗的體重增加到 10 + 5 = 15
並列印出來。
所以結合閉包和高階函式,我們沒有使用 OOP 就有了物件。
可能你在真正寫程式碼的時候不會這樣做,但是知道可以這樣做也是蠻有趣的。確實,閉包被稱為可憐人的物件。
總結
在許多情況下,相比於 OOP 高階函式讓我們可以更好地封裝業務邏輯,我們可以將它們當做資料一樣傳遞。閉包捕獲其周圍環境,幫助我們有效地使用高階函式。
在下一部分,我們將學習如何以函式式的方法去處理錯誤。
如果你喜歡這篇文字,可以點選下面的 按鈕。我通知了他們每一個人,我也感激他們每一個人。
感謝 Abhay Sood 和 s0h4m.
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。