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

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

1. 簡介

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

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

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

必要條件

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

課程內容

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

軟硬體需求

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

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

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

建議您在全螢幕模式下觀看影片 (點選影片右下角的 該符號以醒目顯示的矩形方框標出 4 個角落,表示其處於全螢幕模式。 圖示),以便清楚看見 Android Studio 和程式碼。

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

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 變數中。不過,如要將函式稱為值,就必須使用函式參照運算子 (::)。相關語法如下圖所示:

2afc9eec512244cc.png

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

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

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

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

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

7bf9daf9feff0dec.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 的值是函式。不過,如果您要指定函式參數的類型或傳回類型,就必須知道用來表示函式類型的語法。函式類型由一組包含選用參數清單、-> 符號以及傳回類型的括號組成。相關語法如下圖所示:

1554805ee8183ef.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!

使用函式做為傳回類型

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

6bd674c383827fe5.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. 呼叫 trickFunction(),然後在下一行呼叫 treatFunction()
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() 用作參數的函式也必須能接收本身的參數。宣告函式類型時,參數不會加上標籤。您只需指定每個參數的資料類型,並以半形逗號隔開。相關語法如下圖所示:

8e4c954306a7ab25.png

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

d6dd66beb0d97a99.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 參數數量命名,並使用 -> 運算子將其與函式主體隔開。您現可將 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)? 類型。相關語法如下圖所示:

eb9f645061544a76.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 運算式更簡潔。相關語法如下圖所示:

3fc275a5ad9518be.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 運算式直接傳遞至函式呼叫。相關語法如下圖所示:

2b7175152f7b66e5.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 運算式放在右括號後面,從而呼叫函式。相關語法如下圖所示:

b3de63c209052189.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 迴圈。相關語法如下圖所示:

e0459d5f7c814016.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" 字串應列印 4 次。
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 迴圈類似。

瞭解詳情