初探 Kotlin Lambda 表達式

Jui Yuan Liou
7 min readAug 5, 2018

--

「鵝⋯ 好像也⋯ 沒有想像中複雜?」 圖片來源: Reddit

最近正在學習 Kotlin 的 Lambda 表達式,對於以前習慣使用 Java 的我來說有點陌生。雖然,Java 8 也導入了 Lambda 表達式的支援,寫起來比起過去直接實作匿名類型 (Anonymous Class) 更為簡潔。

看了 Kotlin 的 Lambda 表達式相關的文件後,發現 Kotlin 對於 Lambda 表達式的支援更為強大,並且在語言層級就可以將 Lambda Function 視為變數傳遞。在 Kotlin 的 Standard Library 之中許多 API 就有用到 Lambda Function,例如上次研究過的 T.runT.letT.with ⋯ 等等。甚至一些 String 或 Collections 使用的 API 也是 Lambda Function。

例如 count,API 提示視窗中出現 {…} 就是 Lambda Function

透過這次學習,大致了解了 Lambda 表達式長什麼樣子、要如何使用,首先從 Lambda 表達式本身開始介紹。

Lambda 表達式

Kotlin 裡面,大括號表示的範圍都可以視作一個 Function。例如一般 JVM 程式的進入點 main(argv Array<String>) 是一個 Function。在某一個 Function 裡頭再執行另一個 Function, Lambda 表達式常用來表示這種 Function 內的其他內部 Function。

常常見到的程式碼大概會是:

在其他語言之中,Lambda 表達式又稱作匿名函數 (Anonymous Function),我自己的理解認為 Lambda 表達式是一個沒有名字的 Function。根據前面的理解,在 Kotlin 之中可以將 Lambda 表達式寫成以下的格式:

fun 後面沒有名字,是一個沒有名字的 Function

接下來要來介紹 Kotlin 如何傳遞 Function,並且介紹如何定義的傳遞的 Function。

Higher-Order Functions

上面說道,在 Kotlin 之中可以將 Function 作為變數類型傳遞。換句話說,Function 的地位與其他的 String、Collections、SomeObject 相同,可以用來記錄狀態,同時也能將狀態傳遞給其他人,也能作為回傳值。

要開始傳遞 Function 之前,需要先了解 Function Type,這是一種 Function 的變數類型。

var action: (A, B) -> ReturnType

上面即宣告了一個變數 action ,是一個 Function Type,需要傳入兩個變數類型 A 與 B,回傳的類型是 ReturnType。

括弧內的傳入值可以不傳入任何變數,並且可以傳入最多 22 個變數(Kotlin 1.2 版與之前,Kotlin 1.3 版後可以傳入最多 255 個變數 😅)。不要回傳任何變數, 可以透過 -> Unit 即不會回傳。(像是 Java 的 void)

例如以下的 plus 變數,它是一個 Function Type,有兩個 Int 變數並回傳 Int。

印出的結果是

2 + 3 , result is 5

Kotlin 有個特性是 compiler 自動會判別變數的類型,所以宣告 String 變數可以不用明確定義變數類型,像是:

val firstName = "Jason"    // implied String type

根據這個特性, plus 這個變數也可以寫成:

結果會是一樣的。

一些使用方式

將第一段的曾經出現過的 FindNumber() 作為範例,來看看要怎麼將 Function Type 作為變數傳入其他的 Function 之中。

findNumber() 第二個傳入變數為一個 Function Type,由宣告判斷將會傳入兩個變數,兩者都是 Int,並且會回傳 Int。 findNumber() 可透過 findByCondition 的各種 Function 傳入來取得目標變數,例如第一段的兩個例子便是透過 findByCondition 找出 Collection 中最大的數字與最小的數字。

再看一次第一段的範例,會發現 findNumber() 只有傳入一個變數(一個 randomNumber 的 List),沒看見括弧裡頭的 Function Type ,反而後面直接出現了 Lambda Function。

預期應該會長得這樣才對:

沒錯,不過 IDE 的 inspection info 會告訴我們,可以將括弧內部的 Lambda 表達式搬到外面去。原來,Kotlin 只要最後一個傳入變數 (最右邊) 是 Function Type,就可以搬到括弧外面,自成一個 Scope。

Kotlin 中對於傳入變數還有一個特性,我另外再舉一個例子來說明。這個例子也是隨機產生 0 至 99 間 10 個數字放入 List 之中,從裡面算出大於 50 的數字有幾個。只用 Kotlin 的 Standard Library 即可做到這個效果:

在 Kotlin 中,只要 Lambda 表達式,傳入的變數只有一個,那麼就可以使用 it 來存取。例如 count { number -> number >= 50 },傳入值只有一個 Int ,那麼也可以直接透過 it 來存取 number: Int。

還能透過匿名函數 (Anonymous Function),做出類似 Extension Function 的效果(先假設讀者已經知道)。例如做出為 String 加上兩側字串的 Function —— 送進一個 String,幫它加上括弧。

印出的結果將會是:

This is a !!surrounded!! word.

不過現在看起來與 Extension Function 沒什麼不同 😅。

其他應用

例如在 Android 之中,有時需要保證一些程式碼跑在
Lollipop (5.0, API 21) 以上的版本,處理這類的問題時,習慣做法是每個需要檢查得地方會包在版本檢查的 if 判斷裡頭,再決定是否執行。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODE.LOLLIPOP) {
}

如果利用 Function Type 和 Lambda 表達式則可以這樣寫:

這是設定 5.0 以上狀態列改成透明的程式碼。 runAboveLollipop(action: () -> Unit) Function 傳入值是一個 Function Type,這個 Function 沒有傳入值,也不回傳東西。

將版本判斷放在 runAboveLollipop() Function 之中,只有版本大於 5.0 才會執行傳進來的 Function。

未來在其他地方也需要類似的判斷時,只要這樣寫就行了:

runAboveLollipop {
// Do something above Lollipop
}

此外, Java 的 interface 像 View.OnClickListener ,只有一個 Method —— onClick(View view) 。在 Kotlin 中使用時,可以轉換成 Lambda 表達式來用 (這行為叫 SAM conversions ,SAM: single abstract method )。

view.setOnClickListener {
// it means view
}

--

--