Kotlin Lambda 與高階函式

Kotlin 是 函數式程式語言 (Functional Programming) 的擁護者。 這意味著函式是 一等公民 (First-class citizen):可以被存成變數、當作參數傳遞、也可以當作回傳值。

Lambda 表達式 (Lambda Expression)

Lambda 就是一個「沒有名字的函式」。 語法:{ 參數 -> 程式本體 }

val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2)) // 3

隱式回傳 (Implicit Return)

Lambda 表達式最後一行的執行結果,會自動變成該 Lambda 的回傳值。不需要return

val calculate = { a: Int, b: Int ->
    val result = a * b
    result // 自動回傳 result,不用寫 return result
}

高階函式 (Higher-Order Functions)

接受函式當作參數,或是回傳一個函式的函式,就叫高階函式。 最常見的例子就是集合操作常用的 filter、map。

val numbers = listOf(1, 2, 3, 4, 5)

// 把 lambda 傳給 filter
val evens = numbers.filter { it % 2 == 0 }

唯一的參數 (it)

如果 Lambda 只有一個參數,可以省略參數宣告 x ->,直接用 it 代表那個參數。

// 完整寫法
numbers.map { x -> x * 2 }

// 簡寫 (推薦)
numbers.map { it * 2 }

方法參考 (Member References)

如果你的 Lambda 只是單純呼叫另一個函式,可以使用 :: 運算子來簡化。

fun isEven(x: Int) = x % 2 == 0

val numbers = listOf(1, 2, 3, 4)

// 傳統 Lambda
numbers.filter { isEven(it) }

// 方法參考 (更簡潔)
numbers.filter(::isEven)

未使用的參數 (Underscore for unused variables)

如果 Lambda 有多個參數,但你用不到其中幾個,可以使用底線 _ 來代替變數名稱,告訴編譯器(和閱讀程式碼的人)這個參數被忽略了。

map.forEach { _, value -> 
    println("$value") // 我們只在乎 value,key 用不到
}

解構宣告 (Destructuring Declarations)

如果 Lambda 的參數是 PairMap.Entry 或任何支援解構的資料類別,我們可以直接在參數列進行解構。

val map = mapOf("key1" to 1, "key2" to 2)

// 傳統寫法
map.forEach { entry ->
    println("${entry.key}: ${entry.value}")
}

// 解構寫法 (推薦)
map.forEach { (key, value) ->
    println("$key: $value")
}

尾隨 Lambda (Trailing Lambda)

如果函式的 最後一個參數 是函式,你可以把 Lambda 把移到括號外面。

// 定義 High-Order Function
fun logic(n: Int, operation: (Int) -> Int) {
    println(operation(n))
}

// 呼叫方式 1
logic(5, { it * it })

// 呼叫方式 2 (推薦:尾隨寫法)
logic(5) { 
    it * it 
}

你看,這是不是很像在寫一個語言本身的語法結構?Android 的 Jetpack Compose 就是大量使用這種寫法。

函式型別 (Function Type)

變數可以存函式,那變數的型別是什麼?格式為 (參數型別) -> 回傳型別

val onClick: () -> Unit = { println("Clicked") }
val sum: (Int, Int) -> Int = { a, b -> a + b }

帶有接收者的 Lambda (Lambda with Receiver)

這是 Kotlin 最強大的特性之一,也是打造 DSL (Domain Specific Language) 的基石。 它的型別寫法是 A.() -> B。這表示這個 Lambda 是在「型別 A 的 context」中執行。

  • it (隱式參數):普通的 Lambda 使用 it 來代表傳入的參數。
  • this (隱式接收者):帶有接收者的 Lambda 使用 this 來代表那個接收者物件 (Receiver)。而且 this 可以省略!
// 普通 Lambda: (StringBuilder) -> Unit
val buildStringOld = { sb: StringBuilder ->
    sb.append("Hello")
    sb.append(" World")
}

// 帶有接收者的 Lambda: "StringBuilder.() -> Unit" 
val buildStringNew: StringBuilder.() -> Unit = {
    // 這裡的 this 是 StringBuilder
    this.append("Hello")
    append(" World") // this 可以省略!看起來就像在寫 StringBuilder 內部的程式碼
}

fun main() {
    val sb = StringBuilder()
    buildStringNew(sb) // 呼叫方式 1
    sb.buildStringNew() // 呼叫方式 2 (像擴充函式一樣呼叫)
}

上面的 StringBuilder.() -> Unit 它告訴 Kotlin,這個函式(Lambda)必須在 StringBuilder 的實例(Instance)上執行。() 代表這個函式不需要傳入參數;-> Unit 代表這個函式執行後不回傳任何值。

這就是為什麼 apply, run, with 這些 Scope Functions 可以在 {} 裡面直接呼叫物件的方法,因為它們的參數就是 Lambda with Receiver。

Closure (閉包)

Kotlin 的 Lambda 可以存取並「修改」外部的變數(Java 的 Lambda 只能存取 final 變數)。

var sum = 0
numbers.filter { it > 0 }.forEach {
    sum += it // 直接修改外部變數
}
println(sum)

從 Lambda 返回 (Qualified Return)

Lambda 預設不能使用裸 return (Bare return),因為這會讓編譯器混淆:到底是要從 Lambda 返回,還是從外層的函式返回?

如果你想要從 Lambda 中提早結束,必須使用標籤限制的 return (Qualified Return),語法為 return@label

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)

    println("Start")

    numbers.forEach {
        if (it == 3) {
            // return // 錯誤!這會直接跳出 main() 函式 (如果 forEach 是 inline 的話)
            return@forEach // 正確:只跳出這一次的 Lambda 執行 (相當於 loop 的 continue)
        }
        println(it)
    }

    println("End")
}

forEach 中使用 return@forEach 的行為類似於 continue (跳過當前元素,繼續下一個)。如果你想要類似 break 的效果 (停止整個迴圈),通常建議改用標準的 for 迴圈。