iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Software Development

Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app系列 第 4

Kotlin Lambda 完全入門:從一般函式、匿名函式到高階函式

  • 分享至 

  • xImage
  •  

前幾天我們已經學過變數、條件判斷、迴圈與集合。從今天開始,要進入 Kotlin 非常重要的核心觀念:Lambda 運算式。Lambda 不只常用在集合處理,也是 Android 按鈕事件、RecyclerView 點擊事件、非同步回呼及 Jetpack Compose 的重要基礎。參考資料:Kotlin 官方文件


今天會學到什麼?

  1. 一般具名函式
  2. 單一運算式函式
  3. 函式型別
  4. 函式參考
  5. 匿名函式,也就是俗稱的「無頭騎士」
  6. Lambda 的完整語法
  7. 無參數、單一參數及多參數 Lambda
  8. it 隱含參數
  9. 忽略參數 _
  10. 解構宣告
  11. Lambda 的回傳值
  12. 標籤回傳
  13. Lambda 作為函式參數
  14. 尾隨 Lambda
  15. 高階函式
  16. forEach()filter()map()
  17. Android 點擊事件
  18. Lambda 的優點與使用時機

參考資料:Kotlin 高階函式與 Lambda


一、先從一般函式開始

在認識 Lambda 前,必須先理解一般函式。函式的目的,是將一段具有特定功能的程式碼封裝起來,讓我們可以重複呼叫,避免相同程式碼散落在不同位置。參考資料:Kotlin 函式

fun main() {

    // 呼叫具名函式,並接收函式回傳的結果。
    val result = add(10, 20)

    println("計算結果:$result")
}

/**
 * 將兩個整數相加。
 *
 * @param number1 第一個整數。
 * @param number2 第二個整數。
 * @return 兩個整數相加後的結果。
 */
fun add(number1: Int, number2: Int): Int {

    return number1 + number2
}

執行結果:

計算結果:30

這是一個標準的「具名函式」,它具有函式名稱 add、兩個 Int 參數及一個 Int 回傳值。參考資料:Kotlin 函式回傳值


二、單一運算式函式

如果函式內容只有一個運算式,Kotlin 可以使用 = 簡化函式,省略大括號及 return參考資料:Kotlin 單一運算式函式

原始寫法:

fun add(number1: Int, number2: Int): Int {

    return number1 + number2
}

單一運算式寫法:

fun add(number1: Int, number2: Int): Int =
    number1 + number2

還可以讓 Kotlin 自動推斷回傳型別:

fun add(number1: Int, number2: Int) =
    number1 + number2

完整範例:

fun main() {

    println("加法:${add(10, 5)}")
    println("減法:${subtract(10, 5)}")
    println("乘法:${multiply(10, 5)}")
}

/**
 * 單一運算式函式會自動回傳等號右側的計算結果。
 */
fun add(a: Int, b: Int) = a + b

fun subtract(a: Int, b: Int) = a - b

fun multiply(a: Int, b: Int) = a * b

執行結果:

加法:15
減法:5
乘法:50

單一運算式函式仍然具有名稱,只是寫法比較精簡,因此它還不是 Lambda,也不是匿名函式。參考資料:Kotlin 函式


三、函式也可以是一種資料

在 Kotlin 中,函式不只能被呼叫,還可以像數字及字串一樣存入變數、傳入其他函式,或由另一個函式回傳。參考資料:Kotlin 函式型別

以下是一個一般整數變數:

val number: Int = 100

以下則是一個儲存函式的變數:

val operation: (Int, Int) -> Int

(Int, Int) -> Int 稱為「函式型別」,各部分的意思如下。參考資料:Kotlin 函式型別

(Int, Int) -> Int
 └──┬──┘     └─┬─┘
  參數型別     回傳型別

它表示:

  • 接收第一個 Int
  • 接收第二個 Int
  • 回傳一個 Int

如果不需要參數,可以寫成:

() -> Unit

如果接收一個字串並回傳字串長度,可以寫成:

(String) -> Int

常見函式型別如下。參考資料:Kotlin 函式型別

函式型別 說明
() -> Unit 不接收參數,也不回傳有意義的資料
() -> String 不接收參數,回傳 String
(Int) -> Int 接收一個 Int,回傳 Int
(String) -> Boolean 接收一個 String,回傳 Boolean
(Int, Int) -> Int 接收兩個 Int,回傳 Int
(String, Int) -> Unit 接收 StringInt,不回傳有意義的資料

四、使用函式參考

如果已經有一個具名函式,可以使用雙冒號 :: 取得它的函式參考,再把這個函式存進變數。參考資料:Kotlin 可呼叫參考

fun main() {

    // 使用 ::add 取得 add 函式的參考。
    val operation: (Int, Int) -> Int = ::add

    // 函式型別的變數可以像函式一樣呼叫。
    val result = operation(10, 20)

    println("計算結果:$result")
}

fun add(a: Int, b: Int): Int {

    return a + b
}

執行結果:

計算結果:30

這裡不是立即執行 add(),而是把「如何執行加法」這件事存入 operation。直到執行 operation(10, 20) 時,函式才真正被呼叫。參考資料:Kotlin 函式參考

也可以使用 invoke() 呼叫函式型別的變數:

fun main() {

    val operation: (Int, Int) -> Int = ::add

    // 以下兩種寫法結果相同。
    println(operation(10, 20))
    println(operation.invoke(10, 20))
}

fun add(a: Int, b: Int): Int {

    return a + b
}

執行結果:

30
30

五、匿名函式:沒有名字的「無頭騎士」

具名函式有函式名稱,例如 add;匿名函式則省略函式名稱,只留下 fun、參數及函式內容。因為函式沒有名字,因此可以把它想像成沒有頭的「無頭騎士」。不過它的正式名稱是 Anonymous Function,也就是匿名函式。參考資料:Kotlin 匿名函式

一般具名函式:

fun add(a: Int, b: Int): Int {

    return a + b
}

移除函式名稱後,就成為匿名函式:

fun(a: Int, b: Int): Int {

    return a + b
}

因為匿名函式沒有名稱,通常會把它存進變數,或直接傳給另一個函式。參考資料:Kotlin 匿名函式

fun main() {

    // 將匿名函式存入 add 變數。
    val add: (Int, Int) -> Int =
        fun(a: Int, b: Int): Int {

            return a + b
        }

    println("10 + 20 = ${add(10, 20)}")
}

執行結果:

10 + 20 = 30

六、單一運算式匿名函式

匿名函式也可以使用單一運算式寫法,以 = 直接指定回傳結果。參考資料:Kotlin 匿名函式

fun main() {

    // 匿名函式使用單一運算式形式。
    val add: (Int, Int) -> Int =
        fun(a: Int, b: Int): Int = a + b

    println(add(10, 20))
}

執行結果:

30

如果使用運算式本體,Kotlin 通常可以自動推斷匿名函式的回傳型別。參考資料:Kotlin 匿名函式回傳型別

fun main() {

    val multiply =
        fun(a: Int, b: Int) = a * b

    println(multiply(10, 5))
}

執行結果:

50

七、從匿名函式進化成 Lambda

Lambda 和匿名函式都屬於「函式常值」,也就是沒有先以一般函式宣告,便能直接當成運算式使用的函式。參考資料:Kotlin Lambda 與匿名函式

匿名函式:

val add: (Int, Int) -> Int =
    fun(a: Int, b: Int): Int {

        return a + b
    }

相同功能改用 Lambda:

val add: (Int, Int) -> Int = { a, b ->

    a + b
}

Lambda 會省略 fun、函式名稱、參數的小括號及回傳型別,並使用大括號包住整個函式內容。參考資料:Kotlin Lambda 語法


八、Lambda 的完整語法

Lambda 的完整結構如下。參考資料:Kotlin Lambda 語法

{ 參數名稱: 參數型別 -> 函式內容 }

兩個參數的完整形式:

val add: (Int, Int) -> Int = {
    a: Int,
    b: Int ->

    a + b
}

各部分代表的意義如下:

val add: (Int, Int) -> Int = { a: Int, b: Int -> a + b }
    └─┬─┘  └──────┬──────┘     └────┬────┘    └─┬─┘
    變數名稱       函式型別          參數       執行內容

完整範例:

fun main() {

    // Lambda 接收兩個 Int,最後回傳一個 Int。
    val add: (Int, Int) -> Int = {
        number1: Int,
        number2: Int ->

        number1 + number2
    }

    val result = add(10, 20)

    println("計算結果:$result")
}

執行結果:

計算結果:30

九、Lambda 的型別推斷

Lambda 左側已經寫出完整函式型別時,右側參數通常不必再次標示型別,因為 Kotlin 編譯器可以自動推斷。參考資料:Kotlin Lambda 語法

完整形式:

val add: (Int, Int) -> Int = {
    a: Int,
    b: Int ->

    a + b
}

省略 Lambda 參數型別:

val add: (Int, Int) -> Int = { a, b ->

    a + b
}

也可以省略左側的函式型別,改在 Lambda 參數上標示型別:

val add = { a: Int, b: Int ->

    a + b
}

以下三種形式的功能完全相同:

fun main() {

    // 形式一:左右兩側都標示型別。
    val add1: (Int, Int) -> Int = {
        a: Int,
        b: Int ->

        a + b
    }

    // 形式二:從左側函式型別推斷參數型別。
    val add2: (Int, Int) -> Int = { a, b ->

        a + b
    }

    // 形式三:由右側參數型別推斷整個函式型別。
    val add3 = { a: Int, b: Int ->

        a + b
    }

    println(add1(10, 20))
    println(add2(10, 20))
    println(add3(10, 20))
}

執行結果:

30
30
30

十、沒有參數的 Lambda

如果 Lambda 不需要接收任何參數,函式型別可以寫成 () -> Unit,Lambda 裡也不需要撰寫箭頭。參考資料:Kotlin 函式型別

fun main() {

    // 不接收參數,也不回傳有意義的資料。
    val showMessage: () -> Unit = {

        println("歡迎學習 Kotlin Lambda!")
    }

    showMessage()
}

執行結果:

歡迎學習 Kotlin Lambda!

如果不接收參數,但需要回傳字串,可以寫成 () -> String

fun main() {

    // Lambda 最後一個運算式會成為回傳值。
    val getMessage: () -> String = {

        "歡迎學習 Kotlin"
    }

    println(getMessage())
}

執行結果:

歡迎學習 Kotlin

十一、單一參數 Lambda

只有一個參數時,可以在箭頭左側宣告一個參數。參考資料:Kotlin Lambda 語法

fun main() {

    val square: (Int) -> Int = { number ->

        number * number
    }

    println("10 的平方:${square(10)}")
}

執行結果:

10 的平方:100

十二、使用 it 隱含參數

當 Lambda 只有一個參數,而且編譯器能夠推斷參數型別時,可以省略參數名稱及箭頭,直接以 it 代表該參數。參考資料:Kotlin it 隱含參數

原始形式:

val square: (Int) -> Int = { number ->

    number * number
}

使用 it

val square: (Int) -> Int = {

    it * it
}

完整範例:

fun main() {

    // it 代表傳入的整數。
    val isAdult: (Int) -> Boolean = {

        it >= 18
    }

    println("17 歲成年:${isAdult(17)}")
    println("20 歲成年:${isAdult(20)}")
}

執行結果:

17 歲成年:false
20 歲成年:true

當 Lambda 內容很短時,it 能讓程式更精簡;但如果 Lambda 內容較長或出現巢狀 Lambda,明確命名參數通常會更容易閱讀。參考資料:Kotlin 編碼慣例

val isPassing: (Int) -> Boolean = { score ->

    // 使用 score 比 it 更容易理解這個數字代表成績。
    score >= 60
}

十三、多參數 Lambda

Lambda 可以接收兩個以上的參數,參數之間使用逗號分隔。參考資料:Kotlin Lambda 語法

fun main() {

    val introduce: (String, Int, String) -> String = {
        name,
        age,
        city ->

        "我是 $name,今年 $age 歲,住在 $city。"
    }

    val message = introduce(
        "Alex",
        47,
        "桃園"
    )

    println(message)
}

執行結果:

我是 Alex,今年 47 歲,住在桃園。

多參數 Lambda 不能使用單一的 it 取代全部參數,必須為每個參數指定名稱。參考資料:Kotlin it 隱含參數


十四、使用底線忽略參數

當 Lambda 接收多個參數,但其中某個參數不會被使用時,可以使用底線 _ 忽略它,避免宣告一個不必要的名稱。參考資料:Kotlin 未使用變數

fun main() {

    val users = mapOf(
        1 to "Alex",
        2 to "Mary",
        3 to "John"
    )

    // 不需要使用 Map 的鍵,只需要使用值。
    users.forEach { _, name ->

        println("使用者:$name")
    }
}

執行結果:

使用者:Alex
使用者:Mary
使用者:John

也可以忽略第二個參數:

fun main() {

    val users = mapOf(
        1 to "Alex",
        2 to "Mary",
        3 to "John"
    )

    // 只使用鍵,不使用值。
    users.forEach { id, _ ->

        println("使用者編號:$id")
    }
}

執行結果:

使用者編號:1
使用者編號:2
使用者編號:3

十五、Lambda 參數解構

當 Lambda 取得的資料具有組成部分時,可以直接在參數位置進行解構。例如 Map 的每筆資料包含 keyvalue,可以分別取出來使用。參考資料:Kotlin Lambda 解構

fun main() {

    val scores = mapOf(
        "Alex" to 90,
        "Mary" to 85,
        "John" to 70
    )

    // 將每一筆 Map.Entry 解構為 name 與 score。
    scores.forEach { (name, score) ->

        println("$name 的成績是 $score 分")
    }
}

執行結果:

Alex 的成績是 90 分
Mary 的成績是 85 分
John 的成績是 70 分

十六、Lambda 如何回傳結果?

Lambda 不需要使用一般函式的 return。如果 Lambda 的回傳型別不是 Unit,最後一個運算式會自動成為 Lambda 的回傳值。參考資料:Kotlin Lambda 回傳值

fun main() {

    val calculateLevel: (Int) -> String = { score ->

        // when 是 Lambda 中的最後一個運算式,
        // 因此 when 的結果就是 Lambda 的回傳值。
        when {
            score >= 90 -> "優秀"
            score >= 80 -> "良好"
            score >= 60 -> "及格"
            else -> "不及格"
        }
    }

    println(calculateLevel(95))
    println(calculateLevel(75))
    println(calculateLevel(40))
}

執行結果:

優秀
及格
不及格

如果 Lambda 內有多行程式,仍然只有最後一個運算式會成為回傳結果。參考資料:Kotlin Lambda 回傳值

fun main() {

    val calculatePrice: (Int) -> Int = { originalPrice ->

        val discount = 0.9

        println("原價:$originalPrice")
        println("折扣:九折")

        // 最後一行是 Lambda 的回傳值。
        (originalPrice * discount).toInt()
    }

    val finalPrice = calculatePrice(1000)

    println("折扣價:$finalPrice")
}

執行結果:

原價:1000
折扣:九折
折扣價:900

十七、Lambda 的標籤回傳

如果需要提早結束 Lambda 的某次處理,可以使用限定回傳,例如 return@forEach。它只會離開目前的 Lambda 處理,不會直接結束外層函式。參考資料:Kotlin 限定回傳

fun main() {

    val numbers = listOf(
        10,
        -5,
        20,
        -8,
        30
    )

    numbers.forEach { number ->

        // 遇到負數時跳過目前這筆資料。
        if (number < 0) {
            return@forEach
        }

        println("有效數字:$number")
    }

    println("處理完成")
}

執行結果:

有效數字:10
有效數字:20
有效數字:30
處理完成

return@forEach 的效果類似迴圈中的 continue,表示略過目前元素,繼續處理下一筆資料。參考資料:Kotlin 限定回傳

也可以自訂標籤名稱:

fun main() {

    val numbers = listOf(
        10,
        -5,
        20
    )

    numbers.forEach numberLoop@{ number ->

        // 使用自訂標籤離開目前這次 Lambda。
        if (number < 0) {
            return@numberLoop
        }

        println(number)
    }
}

十八、匿名函式與 Lambda 的 return 差異

匿名函式是使用 fun 宣告的函式,因此匿名函式裡的普通 return 只會結束匿名函式本身。參考資料:Kotlin 匿名函式

fun main() {

    val numbers = listOf(
        10,
        -5,
        20
    )

    numbers.forEach(
        fun(number: Int) {

            // 只結束這一次匿名函式的執行。
            if (number < 0) {
                return
            }

            println("有效數字:$number")
        }
    )

    println("處理完成")
}

執行結果:

有效數字:10
有效數字:20
處理完成

Lambda 裡的 return 規則比較特殊。在支援非區域回傳的行內函式中,沒有標籤的 return 可能會直接結束外層函式,因此初學階段建議使用 return@函式名稱 表達要離開哪個 Lambda。參考資料:Kotlin Return


十九、把 Lambda 傳給函式

函式的參數不只能是 IntString,也可以是一個函式型別。如此一來,呼叫端就能決定函式內部要執行哪一種操作。參考資料:Kotlin 高階函式

fun main() {

    val result = calculate(
        10,
        5,
        { a, b ->
            a + b
        }
    )

    println("計算結果:$result")
}

/**
 * 接收兩個整數及一個計算規則。
 *
 * @param number1 第一個整數。
 * @param number2 第二個整數。
 * @param operation 接收兩個 Int 並回傳 Int 的函式。
 */
fun calculate(
    number1: Int,
    number2: Int,
    operation: (Int, Int) -> Int
): Int {

    // 將資料交給呼叫端傳入的 Lambda 處理。
    return operation(number1, number2)
}

執行結果:

計算結果:15

只要更換傳入的 Lambda,同一個 calculate() 就可以執行不同計算。參考資料:Kotlin 高階函式

fun main() {

    val addResult = calculate(
        10,
        5,
        { a, b -> a + b }
    )

    val subtractResult = calculate(
        10,
        5,
        { a, b -> a - b }
    )

    val multiplyResult = calculate(
        10,
        5,
        { a, b -> a * b }
    )

    println("加法:$addResult")
    println("減法:$subtractResult")
    println("乘法:$multiplyResult")
}

fun calculate(
    number1: Int,
    number2: Int,
    operation: (Int, Int) -> Int
): Int {

    return operation(number1, number2)
}

執行結果:

加法:15
減法:5
乘法:50

二十、什麼是高階函式?

只要函式符合下列任一條件,就稱為高階函式:接收另一個函式作為參數,或回傳另一個函式。參考資料:Kotlin 高階函式

前面的 calculate() 接收 operation 函式參數,因此它就是高階函式:

fun calculate(
    number1: Int,
    number2: Int,
    operation: (Int, Int) -> Int
): Int {

    return operation(number1, number2)
}

高階函式把「資料」與「處理方式」分開,使同一個函式可以搭配不同規則,提升程式的重用性及彈性。參考資料:Kotlin 高階函式


二十一、尾隨 Lambda

如果函式的最後一個參數是函式型別,就可以將 Lambda 移到小括號外,這種形式稱為尾隨 Lambda。參考資料:Kotlin 尾隨 Lambda

原始形式:

val result = calculate(
    10,
    5,
    { a, b -> a + b }
)

尾隨 Lambda:

val result = calculate(10, 5) { a, b ->

    a + b
}

完整範例:

fun main() {

    val result = calculate(10, 5) { a, b ->

        a * b
    }

    println("計算結果:$result")
}

fun calculate(
    number1: Int,
    number2: Int,
    operation: (Int, Int) -> Int
): Int {

    return operation(number1, number2)
}

執行結果:

計算結果:50

如果 Lambda 是函式唯一的參數,連小括號都可以省略。參考資料:Kotlin 尾隨 Lambda

fun main() {

    execute {

        println("開始下載資料")
    }
}

fun execute(action: () -> Unit) {

    println("執行前")

    action()

    println("執行後")
}

執行結果:

執行前
開始下載資料
執行後

二十二、回呼函式 Callback

回呼是指把一段程式傳給另一個函式,等特定工作完成後,再由該函式呼叫這段程式。Lambda 很適合用來撰寫回呼。參考資料:Kotlin 高階函式

fun main() {

    downloadData { result ->

        println("收到下載結果:$result")
    }
}

/**
 * 模擬下載資料。
 *
 * @param onComplete 工作完成後執行的回呼。
 */
fun downloadData(
    onComplete: (String) -> Unit
) {

    println("開始下載……")

    val result = "伺服器資料"

    // 資料準備完成後執行回呼。
    onComplete(result)
}

執行結果:

開始下載……
收到下載結果:伺服器資料

回呼函式能讓執行工作的一方不必知道收到結果後該如何顯示或處理;呼叫端只要傳入合適的 Lambda,就能自行決定下一步。參考資料:Kotlin 高階函式


二十三、Lambda 與 forEach()

forEach() 是一個高階函式,它會依序取出集合元素,並將每個元素交給 Lambda 處理。參考資料:Kotlin 集合操作

完整參數名稱形式:

fun main() {

    val fruits = listOf(
        "Apple",
        "Banana",
        "Orange"
    )

    fruits.forEach { fruit ->

        println("水果:$fruit")
    }
}

使用 it

fun main() {

    val fruits = listOf(
        "Apple",
        "Banana",
        "Orange"
    )

    fruits.forEach {

        println("水果:$it")
    }
}

執行結果:

水果:Apple
水果:Banana
水果:Orange

二十四、Lambda 與 filter()

filter() 會把每個元素交給 Lambda 判斷。如果 Lambda 回傳 true,該元素就會被保留;如果回傳 false,該元素就會被排除。參考資料:Kotlin 集合篩選

fun main() {

    val scores = listOf(
        80,
        45,
        92,
        60,
        30,
        75
    )

    // Lambda 的回傳型別是 Boolean。
    val passingScores = scores.filter { score ->

        score >= 60
    }

    println("原始成績:$scores")
    println("及格成績:$passingScores")
}

執行結果:

原始成績:[80, 45, 92, 60, 30, 75]
及格成績:[80, 92, 60, 75]

使用 it 可以簡化成:

val passingScores = scores.filter {

    it >= 60
}

也可以寫成單行:

val passingScores = scores.filter { it >= 60 }

二十五、Lambda 與 map()

map() 會將集合中的每個元素轉換成新的資料,最後把所有轉換結果組成新的 List參考資料:Kotlin 集合轉換

fun main() {

    val scores = listOf(
        80,
        90,
        70
    )

    // 每一筆成績加 5 分,但最高不超過 100 分。
    val adjustedScores = scores.map { score ->

        (score + 5).coerceAtMost(100)
    }

    println("原始成績:$scores")
    println("調整成績:$adjustedScores")
}

執行結果:

原始成績:[80, 90, 70]
調整成績:[85, 95, 75]

filter() 是決定元素「要不要留下」,而 map() 是決定元素「要變成什麼」。兩者處理目的不同,但都透過 Lambda 讓呼叫端提供規則。參考資料:Kotlin 集合操作


二十六、串接多個 Lambda 集合操作

Kotlin 可以將 filter()map() 及排序函式串接起來,形成清楚的資料處理流程。參考資料:Kotlin 集合操作

fun main() {

    val scores = listOf(
        80,
        45,
        92,
        60,
        30,
        75
    )

    val results = scores
        // 第一階段:只保留及格成績。
        .filter { score ->
            score >= 60
        }
        // 第二階段:每筆成績加 5 分。
        .map { score ->
            (score + 5).coerceAtMost(100)
        }
        // 第三階段:由高到低排序。
        .sortedDescending()

    println("處理結果:$results")
}

執行結果:

處理結果:[97, 85, 80, 65]

這段程式可讀成:「取得成績、篩選及格者、每筆加五分,最後由高到低排序」,比手動建立暫存集合及多個迴圈更容易理解。參考資料:Kotlin 集合操作


二十七、由函式回傳 Lambda

高階函式除了能接收 Lambda,也可以建立並回傳一個 Lambda。參考資料:Kotlin 高階函式

fun main() {

    val tenPercentDiscount =
        createDiscountCalculator(0.9)

    val twentyPercentDiscount =
        createDiscountCalculator(0.8)

    println(
        "九折價格:${tenPercentDiscount(1000)}"
    )

    println(
        "八折價格:${twentyPercentDiscount(1000)}"
    )
}

/**
 * 建立折扣計算函式。
 *
 * @param rate 折扣比例。
 * @return 接收原價並回傳折扣價的 Lambda。
 */
fun createDiscountCalculator(
    rate: Double
): (Int) -> Int {

    return { originalPrice ->

        (originalPrice * rate).toInt()
    }
}

執行結果:

九折價格:900
八折價格:800

回傳的 Lambda 會記住建立時使用的 rate。這種 Lambda 能存取外層範圍變數的能力,稱為閉包。參考資料:Kotlin 閉包


二十八、Lambda 與閉包

Lambda 可以讀取或修改宣告於外層作用域中的變數,這種特性稱為閉包。參考資料:Kotlin 閉包

fun main() {

    var clickCount = 0

    val clickAction: () -> Unit = {

        // Lambda 存取並修改外層的 clickCount。
        clickCount++

        println("目前點擊次數:$clickCount")
    }

    clickAction()
    clickAction()
    clickAction()
}

執行結果:

目前點擊次數:1
目前點擊次數:2
目前點擊次數:3

閉包非常方便,但如果 Lambda 生命週期很長,也需要注意它所保留的外部物件,尤其是在 Android 中不應讓長時間存在的工作不必要地持有 ActivityView參考資料:Android 記憶體管理


二十九、Android 點擊事件中的 Lambda

Android 的 setOnClickListener() 接收點擊事件監聽器。由於 OnClickListener 是只有一個抽象方法的介面,Kotlin 可以使用 SAM 轉換,將 Lambda 轉成監聽器物件。參考資料:Android Kotlin 集合與 Lambda

傳統匿名物件寫法:

button.setOnClickListener(
    object : View.OnClickListener {

        override fun onClick(view: View?) {

            println("按鈕已被點擊")
        }
    }
)

Lambda 寫法:

button.setOnClickListener { view ->

    println("按鈕已被點擊")
}

如果不需要使用傳入的 View,可以省略參數:

button.setOnClickListener {

    println("按鈕已被點擊")
}

Android 官方文件指出,Kotlin 的 SAM 轉換可以讓 OnClickListener 直接使用尾隨 Lambda,程式會比匿名物件更精簡且容易閱讀。參考資料:Android Navigation


三十、Android ViewBinding 點擊事件範例

以下是在 Activity 中搭配 ViewBinding 使用 Lambda 處理按鈕點擊的完整範例。參考資料:Android View Binding

package com.example.lambdademo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.lambdademo.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    /**
     * ViewBinding 物件。
     *
     * 使用 lateinit 是因為 Activity 建立時,
     * Binding 尚未在屬性宣告階段初始化。
     */
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(
        savedInstanceState: Bundle?
    ) {
        super.onCreate(savedInstanceState)

        // 建立 ViewBinding,取得版面中的元件。
        binding =
            ActivityMainBinding.inflate(layoutInflater)

        setContentView(binding.root)

        // 使用 Lambda 設定按鈕點擊事件。
        binding.calculateButton.setOnClickListener {

            val number1 =
                binding.number1EditText.text
                    .toString()
                    .toIntOrNull() ?: 0

            val number2 =
                binding.number2EditText.text
                    .toString()
                    .toIntOrNull() ?: 0

            val result = number1 + number2

            binding.resultTextView.text =
                "計算結果:$result"
        }
    }
}

這裡的 Lambda 代表「使用者點擊按鈕後要執行的工作」。Android 系統會在點擊事件發生時呼叫這段 Lambda,而不是在設定監聽器時立即執行。參考資料:Android UI 事件


三十一、匿名函式與 Lambda 比較

匿名函式和 Lambda 都能建立沒有名稱的函式,但語法及回傳方式有所不同。參考資料:Kotlin 匿名函式

比較項目 匿名函式 Lambda
關鍵字 使用 fun 不使用 fun
參數位置 小括號內 大括號及箭頭左側
明確回傳型別 可以直接宣告 通常依函式型別推斷
回傳方式 可使用普通 return 通常回傳最後一個運算式
尾隨語法 不支援 支援
常見用途 需要明確 return 行為 集合操作、事件、回呼
可讀性 接近一般函式 簡潔,適合短小操作

匿名函式範例:

val checkScore: (Int) -> Boolean =
    fun(score: Int): Boolean {

        return score >= 60
    }

Lambda 範例:

val checkScore: (Int) -> Boolean = { score ->

    score >= 60
}

簡化 Lambda:

val checkScore: (Int) -> Boolean = {

    it >= 60
}

三十二、Lambda 的主要好處

1. 減少重複程式碼

Lambda 可以把經常變動的處理規則傳給共用函式,讓共用函式負責流程,呼叫端只需要決定實際行為。參考資料:Kotlin 高階函式

fun processScore(
    score: Int,
    action: (Int) -> Unit
) {

    action(score)
}

呼叫端可以傳入不同規則:

processScore(90) {
    println("原始成績:$it")
}

processScore(90) {
    println("加分後:${it + 5}")
}

2. 讓函式更有彈性

同一個高階函式可以搭配加法、減法、乘法或其他規則,不必替每一種規則建立一套幾乎相同的函式。參考資料:Kotlin 高階函式

calculate(10, 5) { a, b -> a + b }

calculate(10, 5) { a, b -> a - b }

calculate(10, 5) { a, b -> a * b }

3. 程式更接近自然語意

集合操作搭配 Lambda,可以清楚表達資料篩選、轉換及排序流程。參考資料:Kotlin 集合操作

val result = scores
    .filter { it >= 60 }
    .map { it + 5 }
    .sortedDescending()

4. 適合事件驅動程式

Android App 大量使用點擊、輸入、畫面結果及生命週期事件。Lambda 可以把事件發生後的處理邏輯直接放在設定事件的位置。參考資料:Android UI 事件

binding.loginButton.setOnClickListener {

    viewModel.login()
}

5. 方便實作回呼

需要等待下載、資料庫查詢或使用者操作完成時,可以透過 Lambda 把後續動作傳入處理函式。參考資料:Android Activity Result API

loadUser { user ->

    println("使用者:${user.name}")
}

6. 容易抽換與測試行為

把處理方式設計成函式參數後,正式環境可以傳入真正的操作,測試時則可以傳入簡單的測試 Lambda,降低不同元件之間的耦合。參考資料:Kotlin 高階函式

fun saveData(
    data: String,
    saveAction: (String) -> Boolean
): Boolean {

    return saveAction(data)
}

測試時:

val result = saveData("測試資料") {

    // 測試用 Lambda 不需要真的連接資料庫。
    it.isNotEmpty()
}

三十三、什麼時候不適合使用 Lambda?

Lambda 適合短小、用途明確且只在特定位置使用的程式。如果內容非常長、需要重複使用、包含複雜流程或名稱能明確表達商業意義,改用具名函式通常更容易閱讀與維護。參考資料:Kotlin 編碼慣例

不建議把大量邏輯全部塞進點擊事件:

binding.orderButton.setOnClickListener {

    // 如果這裡包含資料驗證、價格計算、資料庫存取、
    // 網路請求及畫面更新,程式會變得難以維護。
}

比較好的做法是把商業邏輯移到具有意義的具名函式:

binding.orderButton.setOnClickListener {

    submitOrder()
}

/**
 * 驗證並送出訂單。
 */
private fun submitOrder() {

    // 進行訂單驗證及送出處理。
}

Lambda 應該協助程式表達行為,而不是把所有程式壓縮成最短的一行。可讀性及維護性通常比單純減少行數更重要。參考資料:Kotlin 編碼慣例


三十四、各種函式形式總整理

下表整理從一般函式到 Lambda 的演進方式。參考資料:Kotlin Lambda

形式 程式碼
一般具名函式 fun add(a: Int, b: Int): Int { return a + b }
單一運算式函式 fun add(a: Int, b: Int) = a + b
函式參考 val operation = ::add
區塊式匿名函式 val add = fun(a: Int, b: Int): Int { return a + b }
運算式匿名函式 val add = fun(a: Int, b: Int) = a + b
完整 Lambda val add: (Int, Int) -> Int = { a: Int, b: Int -> a + b }
推斷參數型別 val add: (Int, Int) -> Int = { a, b -> a + b }
推斷函式型別 val add = { a: Int, b: Int -> a + b }
單一參數 val square: (Int) -> Int = { number -> number * number }
使用 it val square: (Int) -> Int = { it * it }
無參數 val action: () -> Unit = { println("執行") }
尾隨 Lambda calculate(10, 5) { a, b -> a + b }
忽略參數 map.forEach { _, value -> println(value) }
解構參數 map.forEach { (key, value) -> println("$key=$value") }

三十五、綜合實作:學生成績處理系統

最後使用資料類別、函式型別、高階函式、filter()map() 及尾隨 Lambda,完成一個簡易的學生成績處理系統。參考資料:Kotlin 集合操作

/**
 * 儲存學生資料。
 *
 * @property name 學生姓名。
 * @property score 學生成績。
 */
data class Student(
    val name: String,
    val score: Int
)

fun main() {

    val students = listOf(
        Student("Alex", 80),
        Student("Mary", 45),
        Student("John", 92),
        Student("Amy", 60),
        Student("Peter", 30),
        Student("Linda", 75)
    )

    // 使用高階函式篩選及格學生。
    val passingStudents = selectStudents(
        students
    ) { student ->

        student.score >= 60
    }

    // 將學生資料轉換成顯示用字串。
    val displayResults = passingStudents
        .sortedByDescending { student ->
            student.score
        }
        .map { student ->

            val level = when {
                student.score >= 90 -> "優秀"
                student.score >= 80 -> "良好"
                else -> "及格"
            }

            "${student.name}:${student.score} 分,$level"
        }

    println("===== 及格名單 =====")

    displayResults.forEach { result ->

        println(result)
    }
}

/**
 * 根據外部傳入的判斷規則篩選學生。
 *
 * @param students 原始學生資料。
 * @param condition 學生篩選條件。
 * @return 符合條件的學生清單。
 */
fun selectStudents(
    students: List<Student>,
    condition: (Student) -> Boolean
): List<Student> {

    return students.filter { student ->

        condition(student)
    }
}

執行結果:

===== 及格名單 =====
John:92 分,優秀
Alex:80 分,良好
Linda:75 分,及格
Amy:60 分,及格

這個範例中的 selectStudents() 不需要知道什麼叫「及格」,它只負責依照外部提供的 condition 進行篩選。未來如果要找出滿分學生、需要補考的學生或 80 分以上的學生,只要換掉 Lambda,不需要修改 selectStudents()參考資料:Kotlin 高階函式

// 篩選 80 分以上學生。
val highScoreStudents = selectStudents(
    students
) {
    it.score >= 80
}

// 篩選不及格學生。
val failedStudents = selectStudents(
    students
) {
    it.score < 60
}

// 篩選滿分學生。
val perfectStudents = selectStudents(
    students
) {
    it.score == 100
}

今日總結

Lambda 就是可以直接當成資料使用的函式。學習時可以依照以下順序理解:參考資料:Kotlin Lambda

一般函式
→ 單一運算式函式
→ 函式型別
→ 函式參考
→ 匿名函式
→ Lambda
→ 高階函式
→ 尾隨 Lambda
→ Android 事件與集合操作

今天最重要的幾個語法如下:

// 一般具名函式。
fun add(a: Int, b: Int): Int {
    return a + b
}

// 匿名函式,也就是「無頭騎士」。
val anonymousAdd =
    fun(a: Int, b: Int): Int {
        return a + b
    }

// Lambda 完整形式。
val lambdaAdd1: (Int, Int) -> Int = {
    a: Int,
    b: Int ->

    a + b
}

// Lambda 型別推斷形式。
val lambdaAdd2: (Int, Int) -> Int = { a, b ->
    a + b
}

// 單一參數 Lambda。
val square1: (Int) -> Int = { number ->
    number * number
}

// 使用 it 的單一參數 Lambda。
val square2: (Int) -> Int = {
    it * it
}

// 無參數 Lambda。
val action: () -> Unit = {
    println("開始執行")
}

// 尾隨 Lambda。
calculate(10, 5) { a, b ->
    a + b
}

// Android 點擊事件。
button.setOnClickListener {
    println("按鈕已被點擊")
}

Lambda 的目的不只是縮短程式碼,而是讓「處理方式」可以被儲存、傳遞及替換。掌握 Lambda 後,Kotlin 的集合操作、Android 事件監聽、Jetpack Compose 及許多現代化 API 都會更容易理解。參考資料:Kotlin 高階函式與 Lambda


上一篇
程式不只照順序跑:Kotlin 條件判斷、迴圈與例外處理
下一篇
Kotlin 集合大亂鬥:Array、List、Set、Map,一次搞懂資料怎麼存
系列文
Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言