Android 開發者如何函數語言程式設計 (三)

Android_開發者發表於2017-12-21

在上一章,我們學習了不可變性併發。在這一章,我們將學習高階函式閉包

如果你還沒有閱讀過第一部分和第二部分,可以點選這裡閱讀:

高階函式

高階函式是可以接受將函式作為輸入引數,也可以接受將函式作為輸出結果的一類函式。很酷吧?

但是為什麼有人想要那樣做呢?

讓我們看一個例子。假設我想壓縮一堆檔案。我想用兩種壓縮格式來做 — 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> -> CompressedFilesapplyStrategy 函式。也就是說,它是一個函式,它接受一個檔案列表並返回 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),結果將是 15dog 函式接受了一個 feed 動作,並返回了另外一個真正去給狗餵食的函式。如果把 5 傳遞給這個返回的函式,它將把狗狗的體重增加到 10 + 5 = 15 並列印出來。

所以結合閉包和高階函式,我們沒有使用 OOP 就有了物件。

可能你在真正寫程式碼的時候不會這樣做,但是知道可以這樣做也是蠻有趣的。確實,閉包被稱為可憐人的物件

總結

在許多情況下,相比於 OOP 高階函式讓我們可以更好地封裝業務邏輯,我們可以將它們當做資料一樣傳遞。閉包捕獲其周圍環境,幫助我們有效地使用高階函式。

在下一部分,我們將學習如何以函式式的方法去處理錯誤。

如果你喜歡這篇文字,可以點選下面的 :clap: 按鈕。我通知了他們每一個人,我也感激他們每一個人。

感謝 Abhay Soods0h4m.

掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄


相關文章