在 Kotlin 中使用函式類型和 lambda 運算式

1. 簡介

本程式碼研究室將介紹函式類型、函式類型的使用方式,以及 lambda 運算式特有的語法。

在 Kotlin 中,系統會將函式視為一級結構。這表示函式可視為資料類型。您可將函式儲存在變數中、將函式做為引數傳遞至其他函式,以及從其他函式傳回函式。

如同其他可使用常值表示的資料類型 (例如 10 值的 Int 類型和 "Hello" 值的 String 類型),您也可以宣告函式常值,這通常稱為 lambda 運算式 (簡稱 lambda)。Android 開發作業會廣泛使用 lambda 運算式,而這類運算式在 Kotlin 程式設計中也較為普遍。

必要條件

  • 熟悉 Kotlin 程式設計,包含函式、if/else 陳述式及是否可為空值

課程內容

  • 如何使用 lambda 語法定義函式。
  • 如何將函式儲存在變數中。
  • 如何將函式做為引數傳遞至其他函式。
  • 如何從其他函式傳回函式。
  • 如何使用空值函式類型。
  • 如何讓 lambda 運算式更簡潔。
  • 說明高階函式。
  • 如何使用 repeat() 函式。

軟硬體需求

  • 可使用 Kotlin Playground 的網路瀏覽器。

2. 觀看程式設計示範影片 (可略過)

如果您想觀看課程老師示範完成此程式碼研究室,請觀看以下影片。

建議您在全螢幕模式中觀看影片 (點選影片角落的 這個符號突顯出正方形的 4 個角,用來表示全螢幕模式。 圖示),才能清楚看見 Kotlin Playground 和程式碼。

您可以跳過這個步驟,也可以不觀看這段影片,立即開始進行程式碼研究室的操作步驟。

3. 將函式儲存在變數中

到目前為止,您已經學會如何使用 fun 關鍵字宣告函式。您可以呼叫以 fun 關鍵字宣告的函式,呼叫後,函式主體中的程式碼就會執行。

函式是一級結構,屬於資料類型,因此您可以將函式儲存在變數中、將函式傳遞至函式,以及從函式傳回函式。也許您希望能夠變更應用程式在執行階段時的部分行為,或是為可組合函式建立巢狀結構以建立版面配置,就像您在先前程式碼研究室中的做法一樣。有了 lambda 運算式,這一切都能實現。

您可以透過「不給糖就搗蛋」活動,瞭解這個運算式的實際使用狀況;這個活動是許多國家/地區的「萬聖節」傳統活動;每逢這個節日,孩子們會穿著奇裝異服,挨家挨戶敲門,並喊出「不給糖就搗蛋」口號 (通常是為了討得糖果)。

將函式儲存在變數中:

  1. 前往 Kotlin Playground
  2. main() 函式後方,定義不含參數和回傳值的 trick() 函式,該函式會顯示 "No treats!"。其語法與您在先前的程式碼研究室中看到的其他函式的語法相同。
fun main() {

}

fun trick() {
    println("No treats!")
}
  1. main() 函式的主體中,建立稱為 trickFunction 的變數,並將其設定為與 trick 等同。請不要在 trick 之後加上括號,因為您是想將函式儲存在變數中,而非呼叫函式。
fun main() {
    val trickFunction = trick
}

fun trick() {
    println("No treats!")
}
  1. 執行程式碼。這會產生錯誤,因為 Kotlin 編譯器會將 trick 識別為 trick() 函式的名稱,但它會假設您要呼叫函式,而不是將其指派給變數。
Function invocation 'trick()' expected

您曾嘗試將 trick 儲存在 trickFunction 變數中。不過,如要將函式稱為值,就需要使用函式參照運算子 (::)。相關語法如下圖所示:

a9a9bfa88485ec67.png

  1. 如要將函式稱為值,請將 trickFunction 重新指派給 ::trick
fun main() {
    val trickFunction = ::trick
}

fun trick() {
    println("No treats!")
}
  1. 執行程式碼,驗證是否沒有其他錯誤。系統顯示一則警告訊息:trickFunction 從未使用過,但在下一個部分中顯示已修正。

使用 lambda 運算式重新定義函式

Lambda 運算式提供了簡潔的語法,用來定義不含 fun 關鍵字的函式。您可以將 lambda 運算式直接儲存在變數中,無需參照另一個函式的函式。

在指派運算子 (=) 的前面新增 valvar 關鍵字,後面加上變數名稱,以供您呼叫函數時使用。在指派運算子 (=) 之後的是 lambda 運算式,由一組構成函式主體的大括號組成。相關語法如下圖所示:

5e25af769cc200bc.png

使用 lambda 運算式定義函式時,變數會參照該函式。您也可以將其值指派給其他變數 (例如任何其他類型),並使用新變數的名稱來呼叫函式。

如要使用 lambda 運算式,請更新程式碼:

  1. 使用 lambda 運算式重新編寫 trick() 函式。trick 這個名稱現指的是變數名稱。大括號中的函式主體現在是 lambda 運算式。
fun main() {
    val trickFunction = ::trick
}

val trick = {
    println("No treats!")
}
  1. main() 函式中,移除函式參照運算子 (::),因為 trick 現在參照的是變數,而非函式名稱。
fun main() {
    val trickFunction = trick
}

val trick = {
    println("No treats!")
}
  1. 執行程式碼。沒有發生任何錯誤,因此您可以在沒有函式參照運算子 (::) 的情況下參照 trick() 函式。您還沒有呼叫該函式,因此沒有輸出結果。
  2. main() 函式中,呼叫 trick() 函式,但這次請加入括號,就像您呼叫任何其他函式時的做法一樣。
fun main() {
    val trickFunction = trick
    trick()
}

val trick = {
    println("No treats!")
}
  1. 執行程式碼。系統會執行 lambda 運算式的主體。
No treats!
  1. main() 函式中,呼叫 trickFunction 變數,就像呼叫函式一樣。
fun main() {
    val trickFunction = trick
    trick()
    trickFunction()
}

val trick = {
    println("No treats!")
}
  1. 執行程式碼。系統會針對 trick() 函式呼叫和 trickFunction() 函式呼叫分別呼叫一次該函式。
No treats!
No treats!

透過 lambda 運算式,您可以建立用來儲存函式的變數、以呼叫函式的方法呼叫這些變數,並將這些變數儲存在其他可像函式一樣呼叫的變數中。

4. 將函式做為資料類型

在先前的程式碼研究室中,您已經瞭解到 Kotlin 擁有類型推論。宣告變數時,通常不需要明確指定類型。在上述範例中,Kotlin 編譯器能推論出 trick 的值是函式。不過,如果您要指定函式參數的類型或傳回類型,就必須瞭解用來表示函式類型的語法。函式類型由一組括號組成,其中包含選用參數清單、-> 符號以及傳回類型。相關語法如下圖所示:

5608ac5e471b424b.png

您先前所宣告 trick 變數的資料類型為 () -> Unit。函式沒有任何參數,因此括號是空的。傳回類型為 Unit,因為函式未傳回任何內容。如果您的函式使用兩個 Int 參數並傳回 Int,則它的資料類型為 (Int, Int) -> Int

使用 lambda 運算式宣告另一個函式,以便明確指定該函式的類型:

  1. trick 變數之後,宣告 treat 變數,此次宣告的變數等於主體可顯示 "Have a treat!" 的 lambda 運算式。
val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}
  1. treat 變數的資料類型指定為 () -> Unit
val treat: () -> Unit = {
    println("Have a treat!")
}
  1. main() 函式中呼叫 treat() 函式。
fun main() {
    val trickFunction = trick
    trick()
    trickFunction()
    treat()
}
  1. 執行程式碼。treat() 函式的運作方式與 trick() 函式相同。雖然只有 treat 變數明確宣告過,但這兩個變數的資料類型相同。
No treats!
No treats!
Have a treat!

使用函式做為傳回類型

函式是一種資料類型,因此可以像任何其他資料類型一樣使用函式。您甚至可以從其他函式傳回函式。相關語法如下圖所示:

f16dd6ca0c1588f5.png

建立可傳回函式的函式。

  1. 刪除 main() 函式中的程式碼。
fun main() {

}
  1. main() 函式之後,定義可接受類型為 BooleanisTrick 參數的 trickOrTreat() 函式。
fun main() {

}

fun trickOrTreat(isTrick: Boolean): () -> Unit {
}

val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}
  1. trickOrTreat() 函式的主體中,新增 if 陳述式,使其在 isTricktrue 時傳回 trick() 函式;如果 isTrick 為「false」,則傳回 treat()
fun trickOrTreat(isTrick: Boolean): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        return treat
    }
}
  1. main() 函式中,建立稱為 treatFunction 的變數並將其指派給呼叫 trickOrTreat() 的結果,然後在 isTrick 參數中傳入 false。接著建立稱為 trickFunction 的第二個變數,然後將其指派給呼叫 trickOrTreat() 的結果,這次要在 isTrick 參數中傳入 true
fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
}
  1. 呼叫 treatFunction(),然後在下一行呼叫 trickFunction()
fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. 執行程式碼。您應該會看到每個函式的輸出結果。即使您無法直接呼叫 trick()treat() 函式,仍然可以呼叫這些函式,因為在您每次呼叫 trickOrTreat() 函式以及透過 trickFunctiontreatFunction 變數呼叫函式時,系統都會儲存回傳值。
Have a treat!
No treats!

現在,您已瞭解函式如何傳回其他函式。您也可以將函式做為引數傳遞至其他函式。您可能會想為 trickOrTreat() 函式提供部分自訂行為,讓其執行傳回兩個字串之一以外的操作。如果函式將另一個函式做為引數,則您每次呼叫函數時,都可以傳入一個不同的函式。

將函式做為引數傳遞至其他函式

歡慶萬聖節的時候,部分地區會用零錢取代糖果,或者同時為孩子們準備零錢與糖果。您必須修改 trickOrTreat() 函式,以便將函式代表的額外 treat (點心) 做為引數來提供。

trickOrTreat() 用作參數的函式也必須能接收本身的參數。宣告函式類型時,參數不會加上標籤。您只需指定每個參數的資料類型,並以半形逗號隔開。相關語法如下圖所示:

8372d3b83d539fac.png

您為接收參數的函式編寫 lambda 運算式時,系統會按照參數的出現順序為參數命名。參數名稱會列在左大括號後面,各名稱以半形逗號分隔。箭頭 (->) 可將參數名稱與函式主體隔開。相關語法如下圖所示:

938d2adf25172873.png

如要以參數的形式接收函式,請更新 trickOrTreat() 函式:

  1. isTrick 參數之後,新增類型 (Int) -> StringextraTreat 參數。
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
  1. else 區塊中,於 return 陳述式之前呼叫 println(),然後傳入 extraTreat() 函式的呼叫。將 5 傳遞到 extraTreat() 的呼叫中。
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        println(extraTreat(5))
        return treat
    }
}
  1. 現在,當您呼叫 trickOrTreat() 函式時,必須定義一個包含 lambda 運算式的函式,並將其傳入 extraTreat 參數。在呼叫 trickOrTreat() 函式之前的 main() 函式中,新增 coins() 函式。coins() 函式為 Int 參數提供名稱 quantity 並傳回 String。您可能注意到缺少「return」關鍵字,因為該關鍵字不能用於 lambda 運算式。而這個函式中最後一個運算式的結果會成為回傳值。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. coins() 函式後方新增 cupcake() 函式,如下所示。將 Int 參數命名為 quantity,並使用 -> 運算子分隔此參數和函式主體。您現可將 coins()cupcake() 函式傳遞至 trickOrTreat() 函式。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val cupcake: (Int) -> String = { quantity ->
        "Have a cupcake!"
    }

    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. cupcake() 函式中,移除 quantity 參數和 -> 符號。這兩個內容並未使用,因此可以略過。
val cupcake: (Int) -> String = {
    "Have a cupcake!"
}
  1. 將呼叫更新為 trickOrTreat() 函式。如果是第一次呼叫,當 isTrickfalse 時,請傳入 coins() 函式。如果是第二次呼叫,當 isTricktrue 時,請傳入 cupcake() 函式。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val cupcake: (Int) -> String = {
        "Have a cupcake!"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, cupcake)
    treatFunction()
    trickFunction()
}
  1. 執行程式碼。只有在 isTrick 參數設為 false 引數時才會呼叫 extraTreat() 函式,因此輸出結果包含 5 個 quarter,但不包括 cupcake。
5 quarters
Have a treat!
No treats!

可為空值函式類型

如同其他資料類型,函式類型可宣告為「空值」。在這些情況下,變數可能包含函式,也可能是 null

如要將函式宣告為可為空值,請用括號括住函式類型,然後在右括號外加上 ? 符號。舉例來說,如要將 () -> String 類型設為空值,請將其宣告為 (() -> String)? 類型。相關語法如下圖所示:

c8a004fbdc7469d.png

extraTreat 參數設為空值,這樣您就不必在每次呼叫 trickOrTreat() 函式時提供 extraTreat() 函式:

  1. extraTreat 參數的類型變更為 (() -> String)?
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
  1. 修改 extraTreat() 函式的呼叫,以使用 if 陳述式只呼叫非空值的函式。trickOrTreat() 函式現在應如下列程式碼片段所示:
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        if (extraTreat != null) {
            println(extraTreat(5))
        }
        return treat
    }
}
  1. 移除 cupcake() 函式,然後在第二次呼叫 trickOrTreat() 函式時,使用 null 取代 cupcake 引數。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. 執行程式碼。輸出結果保持不變。現在您可以將函式類型宣告為可為空值,不必再將函式傳入 extraTreat 參數。
5 quarters
Have a treat!
No treats!

5. 使用簡短語法編寫 lambda 運算式

Lambda 運算式提供多種方式,可讓程式碼更簡潔。在本節中,您可以瞭解其中幾種方式,因為您遇到和編寫的 lambda 運算式大多都是以簡短語法寫成。

省略參數名稱

先前編寫 coins() 函式時,您已為函式的 Int 參數明確宣告名稱 quantity。不過,如 cupcake() 函式所示,您可以完全省略參數名稱。如果函式只有一個參數,且您未提供名稱,Kotlin 會以隱含形式指定 it 名稱,因此您可以省略參數名稱和 -> 符號,讓 lambda 運算式更加簡潔。相關語法如下圖所示:

332ea7bade5062d6.png

如要為參數使用簡短語法,請更新 coins() 函式:

  1. coins() 函式中,移除 quantity 參數名稱和 -> 符號。
val coins: (Int) -> String = {
    "$quantity quarters"
}
  1. 變更 "$quantity quarters" 字串範本,以使用 $it 參照單一參數。
val coins: (Int) -> String = {
    "$it quarters"
}
  1. 執行程式碼。Kotlin 可辨識 Int 參數的 it 參數名稱,並且仍顯示 quarter 的數量。
5 quarters
Have a treat!
No treats!

將 lambda 運算式直接傳遞至函式

coins() 函式目前只能在同一處中使用。如果您只需將 lambda 運算式直接傳入 trickOrTreat() 函式,而不必先建立變數,該怎麼做?

Lambda 運算式只是函式常值,就像 0 是整數常值,"Hello" 則是字串常值。您可以將 lambda 運算式直接傳遞至函式呼叫。相關語法如下圖所示:

39dc1086e2471ffc.png

請修改程式碼,以便移除 coins 變數:

  1. 移動 lambda 運算式,讓該運算式直接傳入對 trickOrTreat() 函式的呼叫。您也可以將 lambda 運算式串連為單行。
fun main() {
    val coins: (Int) -> String = {
        "$it quarters"
    }
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. 移除 coins 變數,因為它無法再使用。
fun main() {
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. 執行程式碼。該程式碼仍會如預期編譯和執行。
5 quarters
Have a treat!
No treats!

使用結尾的 lambda 語法

當函式類型為函式的最後一個參數時,您可以使用另一個簡短選項來編寫 lambda。如此一來,您便可將 lambda 運算式放在右括號後面,從而呼叫函式。相關語法如下圖所示:

3ee3176d612b54.png

這會讓程式碼更容易閱讀,因為該語法會分隔 lambda 運算式與其他參數,但不會改變程式碼的用途。

如要使用結尾的 lambda 語法,請更新程式碼:

  1. treatFunction 變數中,將 lambda 運算式 {"$it quarters"} 移至 trickOrTreat() 呼叫的右括號後面。
val treatFunction = trickOrTreat(false) { "$it quarters" }
  1. 執行程式碼。一切都還能正常運作!
5 quarters
Have a treat!
No treats!

6. 使用 repeat() 函式

如果某個函式會傳回函式「或」將函式做為引數,則稱為高階函式。trickOrTreat() 函式就是一種高階,因為此函式會接受 ((Int) -> String)? 類型的函式做為參數,並傳回 () -> Unit 類型的函式。Kotlin 提供多個實用高階,您可以使用新學的 lambda 概念善用這些函式。

repeat() 函式就是這種高階。repeat() 函式可讓您以簡潔的方式,使用函式表示 for 迴圈。在後續單元,您會頻繁地使用此函式及其他高階函式。repeat() 函式具有以下函式簽章:

repeat(times: Int, action: (Int) -> Unit)

times 參數是指該動作應發生的次數。action 參數是一個會接收單一 Int 參數並傳回 Unit 類型的函式。action 函式的 Int 參數是指動作目前為止執行的次數,例如第一次疊代的 0 引數或第二次疊代的 1 引數。您可以使用 repeat() 函式,按指定次數重複執行程式碼,與 for 迴圈類似。相關語法如下圖所示:

519a2e0f5d02687.png

您可以使用 repeat() 函式多次呼叫 trickFunction() 函式,而非只能呼叫一次。

如要查看 repeat() 函式的實際使用狀況,請更新「不給糖就搗蛋」程式碼:

  1. main() 函式中,呼叫 treatFunction()trickFunction() 呼叫之間的 repeat() 函式。在 times 參數傳入 4,並在 action 函式中使用結尾的 lambda 語法。您不需要為 lambda 運算式的 Int 參數提供名稱。
fun main() {
    val treatFunction = trickOrTreat(false) { "$it quarters" }
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
    repeat(4) {

    }
}
  1. treatFunction() 函式的呼叫移至 repeat() 函式的 lambda 運算式中。
fun main() {
    val treatFunction = trickOrTreat(false) { "$it quarters" }
    val trickFunction = trickOrTreat(true, null)
    repeat(4) {
        treatFunction()
    }
    trickFunction()
}
  1. 執行程式碼。"Have a treat" 字串應輸出四次。
5 quarters
Have a treat!
Have a treat!
Have a treat!
Have a treat!
No treats!

7. 結語

恭喜!您已瞭解函式類型和 lambda 運算式的基本概念。熟悉這些概念有助於您深入瞭解 Kotlin 語言。此外,運用函式類型、高階函式和簡短語法,還能讓程式碼更簡潔易讀。

摘要

  • Kotlin 中的函式是一級函式結構,並且可視為資料類型。
  • Lambda 運算式提供了編寫函式所需的簡短語法。
  • 您可以將函式類型傳遞至其他函式。
  • 您可以從其他函式傳回函式類型。
  • lambda 運算式會傳回最後一個運算式的值。
  • 如果具有單一參數的 lambda 運算式省略某個參數標籤,則系統會使用 it ID 來參照它。
  • 無需變數名稱,即可用內嵌方式編寫 lambda。
  • 如果函式的最後一個參數是函式類型,您可以在呼叫函式時,使用結尾的 lambda 語法,將 lambda 運算式移至最後一組括號後面。
  • 高階是指將其他函式做為參數的函式,或是可傳回函式的函式。
  • repeat() 函式是一個高階函式,其運作方式與 for 迴圈類似。

瞭解詳情