在前面一系列的 Collection 操作章節裡,你會發現只要能客製化操作行為的 method,都是透過讓你傳入一個 Lambda 的方式達成。到底什麼是 Lambda?Collection 核心又是如何利用 Lambda?我們將在這章深入探討。
一般我們在定義函式的時候,只要用 fun
關鍵字,加上函式名稱、以 ()
宣告參數、以 {}
括住函式的所有動作即可。
fun hello() {
// ...
}
Kotlin 支援一種特別的函式稱為 Lambda,這種函式不需要定義名字,又常被稱為匿名函數。Lambda 是一套數學演算邏輯,由數學家 Alonzo Church 於 20 世紀 30 年代發明,可以用希臘字元 λ
表示。當定義匿名函數時,就使用了 Lambda 演算法。
{
val name = "Shengyou"
println("Hello, $name")
}()
我們可以看到上面這段程式碼沒有 fun
關鍵字、沒有函式的名字,只用 {}
定義函式行為並以 ()
直接執行。
當然,我們也可以把 Lambda 儲存在變數裡,等到需要的再呼叫。
val myGreetingFun: () -> Unit = {
val name = "Shengyou"
println("Hello, $name")
}
myGreetingFun()
跟上例不同,把 Lambda 宣告在變數時不需要加上 ()
。而呼叫時則跟函式相同,以變數的名稱加上 ()
即可執行。
注意一下變數的型別,不是常見的標準型別,而是 () -> Unit
。這代表變數的型別是一個函式類別(Function Type),()
代表函式本身、->
代表函式執行、Unit
代表函式回傳的型別。
Lambda 可以存在變數裡,也可以當成參數傳入函式裡。在宣告參數型別時,跟宣告變數型別一樣,採用 () -> Type
的格式標明。
fun hello(name: String, greeting: (String) -> String): String {
return greeting(name)
}
呼叫這個函式時,要把所需的 name
字串及 greeting
函式傳入。若 Lambda 是函式的最後一個參數,則可以將函式本體放在 ()
的外面;若 Lambda 是函式的唯一一個參數,甚至可以直接省略 ()
。
// 呼叫函式
hello("Shengyou", {
// ...
})
// 將最後一個參數 Lambda 放到 `()` 外面
hello("Shengyou") {
// ...
}
it
關鍵字若 Lambda 本身只接受一個參數,則在 Lambda 內部可以用 it
來表示該參數。假如 Lambda 動作單純,可以用 it 關鍵字讓程式碼更精簡。不過若是 Lambda 有多個參數、或是遇到巢狀 Lambda 時,建議還是顯式的標示參數名稱以提升程式可讀性。
hello("Shengyou") {
"Hello, $it" // it 就代表 Lambda 拿到的 name 參數,在此例裡就是 Shengyou
}
在上面的範例裡,你會發現雖然 Lambda 是函式的一種,也有定義回傳型別,但卻不用 return
關鍵字?沒錯!這是因為 Lambda 會自動回傳本體最後一行語句的結果。
inline
機制雖然 Lambda 很靈活好用,但因為在 JVM 上它是以物件實例的型式存在,換句話說,所有和 Lambda 互動的變數都會產生記憶體開銷,長期下來會有效能問題。
為了解決這個問題 Kotlin 提供 inline 機制,只要在 fun
關鍵字前加上 inline
,編譯器就會將 Lambda 本體直接複製貼到那裡,如此一來就能避免 Lambda 函數的傳遞,也不需要建立新的物件實例了。
forEach()
原始碼有了以上的語法基礎,再看 Collection 的原始碼就能秒懂其運作原理。這邊以 List 的 forEach()
為例:
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
我們可以看到 forEach
這個函式只接受一個名為 action
的 Lambda 參數並回傳 Unit,而這個 action
接受一個泛型參數並回傳 Unit,在函式本體裡,用 for
將 Collection 逐一取出成 element
,每一個 element
都傳給 action
做處理。而 forEach
有使用到 Lambda,因此有加上 inline
宣告。
看懂愈多 Collection 的原始碼,是不是覺得 Collection 的實作非常精妙呢?