1. 簡介
本程式碼研究室將介紹函式類型、函式類型的使用方式,以及 lambda 運算式特有的語法。
在 Kotlin 中,系統會將函式視為一級結構。這表示函式可視為資料類型。您可將函式儲存在變數中、將函式做為引數傳遞至其他函式,以及從其他函式傳回函式。
如同其他可使用常值表示的資料類型 (例如 10
值的 Int
類型和 "Hello"
值的 String
類型),您也可以宣告函式常值,這通常稱為 lambda 運算式 (簡稱 lambda)。Android 開發作業會廣泛使用 lambda 運算式,而這類運算式在 Kotlin 程式設計中也較為普遍。
必要條件
- 熟悉 Kotlin 程式設計,包含函式、
if/else
陳述式及是否可為空值
課程內容
- 如何使用 lambda 語法定義函式。
- 如何將函式儲存在變數中。
- 如何將函式做為引數傳遞至其他函式。
- 如何從其他函式傳回函式。
- 如何使用空值函式類型。
- 如何讓 lambda 運算式更簡潔。
- 說明高階函式。
- 如何使用
repeat()
函式。
軟硬體需求
- 可使用 Kotlin Playground 的網路瀏覽器。
2. 觀看程式設計示範影片 (可略過)
如果您想觀看課程老師示範完成此程式碼研究室,請觀看以下影片。
建議您在全螢幕模式中觀看影片 (點選影片角落的 圖示),才能清楚看見 Kotlin Playground 和程式碼。
您可以跳過這個步驟,也可以不觀看這段影片,立即開始進行程式碼研究室的操作步驟。
3. 將函式儲存在變數中
到目前為止,您已經學會如何使用 fun
關鍵字宣告函式。您可以呼叫以 fun
關鍵字宣告的函式,呼叫後,函式主體中的程式碼就會執行。
函式是一級結構,屬於資料類型,因此您可以將函式儲存在變數中、將函式傳遞至函式,以及從函式傳回函式。也許您希望能夠變更應用程式在執行階段時的部分行為,或是為可組合函式建立巢狀結構以建立版面配置,就像您在先前程式碼研究室中的做法一樣。有了 lambda 運算式,這一切都能實現。
您可以透過「不給糖就搗蛋」活動,瞭解這個運算式的實際使用狀況;這個活動是許多國家/地區的「萬聖節」傳統活動;每逢這個節日,孩子們會穿著奇裝異服,挨家挨戶敲門,並喊出「不給糖就搗蛋」口號 (通常是為了討得糖果)。
將函式儲存在變數中:
- 前往 Kotlin Playground。
- 在
main()
函式後方,定義不含參數和回傳值的trick()
函式,該函式會顯示"No treats!"
。其語法與您在先前的程式碼研究室中看到的其他函式的語法相同。
fun main() {
}
fun trick() {
println("No treats!")
}
- 在
main()
函式的主體中,建立稱為trickFunction
的變數,並將其設定為與trick
等同。請不要在trick
之後加上括號,因為您是想將函式儲存在變數中,而非呼叫函式。
fun main() {
val trickFunction = trick
}
fun trick() {
println("No treats!")
}
- 執行程式碼。這會產生錯誤,因為 Kotlin 編譯器會將
trick
識別為trick()
函式的名稱,但它會假設您要呼叫函式,而不是將其指派給變數。
Function invocation 'trick()' expected
您曾嘗試將 trick
儲存在 trickFunction
變數中。不過,如要將函式稱為值,就需要使用函式參照運算子 (::
)。相關語法如下圖所示:
- 如要將函式稱為值,請將
trickFunction
重新指派給::trick
。
fun main() {
val trickFunction = ::trick
}
fun trick() {
println("No treats!")
}
- 執行程式碼,驗證是否沒有其他錯誤。系統顯示一則警告訊息:
trickFunction
從未使用過,但在下一個部分中顯示已修正。
使用 lambda 運算式重新定義函式
Lambda 運算式提供了簡潔的語法,用來定義不含 fun
關鍵字的函式。您可以將 lambda 運算式直接儲存在變數中,無需參照另一個函式的函式。
在指派運算子 (=
) 的前面新增 val
或 var
關鍵字,後面加上變數名稱,以供您呼叫函數時使用。在指派運算子 (=
) 之後的是 lambda 運算式,由一組構成函式主體的大括號組成。相關語法如下圖所示:
使用 lambda 運算式定義函式時,變數會參照該函式。您也可以將其值指派給其他變數 (例如任何其他類型),並使用新變數的名稱來呼叫函式。
如要使用 lambda 運算式,請更新程式碼:
- 使用 lambda 運算式重新編寫
trick()
函式。trick
這個名稱現指的是變數名稱。大括號中的函式主體現在是 lambda 運算式。
fun main() {
val trickFunction = ::trick
}
val trick = {
println("No treats!")
}
- 在
main()
函式中,移除函式參照運算子 (::
),因為trick
現在參照的是變數,而非函式名稱。
fun main() {
val trickFunction = trick
}
val trick = {
println("No treats!")
}
- 執行程式碼。沒有發生任何錯誤,因此您可以在沒有函式參照運算子 (
::
) 的情況下參照trick()
函式。您還沒有呼叫該函式,因此沒有輸出結果。 - 在
main()
函式中,呼叫trick()
函式,但這次請加入括號,就像您呼叫任何其他函式時的做法一樣。
fun main() {
val trickFunction = trick
trick()
}
val trick = {
println("No treats!")
}
- 執行程式碼。系統會執行 lambda 運算式的主體。
No treats!
- 在
main()
函式中,呼叫trickFunction
變數,就像呼叫函式一樣。
fun main() {
val trickFunction = trick
trick()
trickFunction()
}
val trick = {
println("No treats!")
}
- 執行程式碼。系統會針對
trick()
函式呼叫和trickFunction()
函式呼叫分別呼叫一次該函式。
No treats! No treats!
透過 lambda 運算式,您可以建立用來儲存函式的變數、以呼叫函式的方法呼叫這些變數,並將這些變數儲存在其他可像函式一樣呼叫的變數中。
4. 將函式做為資料類型
在先前的程式碼研究室中,您已經瞭解到 Kotlin 擁有類型推論。宣告變數時,通常不需要明確指定類型。在上述範例中,Kotlin 編譯器能推論出 trick
的值是函式。不過,如果您要指定函式參數的類型或傳回類型,就必須瞭解用來表示函式類型的語法。函式類型由一組括號組成,其中包含選用參數清單、->
符號以及傳回類型。相關語法如下圖所示:
您先前所宣告 trick
變數的資料類型為 () -> Unit
。函式沒有任何參數,因此括號是空的。傳回類型為 Unit
,因為函式未傳回任何內容。如果您的函式使用兩個 Int
參數並傳回 Int
,則它的資料類型為 (Int, Int) -> Int
。
使用 lambda 運算式宣告另一個函式,以便明確指定該函式的類型:
- 在
trick
變數之後,宣告treat
變數,此次宣告的變數等於主體可顯示"Have a treat!"
的 lambda 運算式。
val trick = {
println("No treats!")
}
val treat = {
println("Have a treat!")
}
- 將
treat
變數的資料類型指定為() -> Unit
。
val treat: () -> Unit = {
println("Have a treat!")
}
- 在
main()
函式中呼叫treat()
函式。
fun main() {
val trickFunction = trick
trick()
trickFunction()
treat()
}
- 執行程式碼。
treat()
函式的運作方式與trick()
函式相同。雖然只有treat
變數明確宣告過,但這兩個變數的資料類型相同。
No treats! No treats! Have a treat!
使用函式做為傳回類型
函式是一種資料類型,因此可以像任何其他資料類型一樣使用函式。您甚至可以從其他函式傳回函式。相關語法如下圖所示:
建立可傳回函式的函式。
- 刪除
main()
函式中的程式碼。
fun main() {
}
- 在
main()
函式之後,定義可接受類型為Boolean
的isTrick
參數的trickOrTreat()
函式。
fun main() {
}
fun trickOrTreat(isTrick: Boolean): () -> Unit {
}
val trick = {
println("No treats!")
}
val treat = {
println("Have a treat!")
}
- 在
trickOrTreat()
函式的主體中,新增if
陳述式,使其在isTrick
是true
時傳回trick()
函式;如果isTrick
為「false」,則傳回treat()
。
fun trickOrTreat(isTrick: Boolean): () -> Unit {
if (isTrick) {
return trick
} else {
return treat
}
}
- 在
main()
函式中,建立稱為treatFunction
的變數並將其指派給呼叫trickOrTreat()
的結果,然後在isTrick
參數中傳入false
。接著建立稱為trickFunction
的第二個變數,然後將其指派給呼叫trickOrTreat()
的結果,這次要在isTrick
參數中傳入true
。
fun main() {
val treatFunction = trickOrTreat(false)
val trickFunction = trickOrTreat(true)
}
- 呼叫
treatFunction()
,然後在下一行呼叫trickFunction()
。
fun main() {
val treatFunction = trickOrTreat(false)
val trickFunction = trickOrTreat(true)
treatFunction()
trickFunction()
}
- 執行程式碼。您應該會看到每個函式的輸出結果。即使您無法直接呼叫
trick()
或treat()
函式,仍然可以呼叫這些函式,因為在您每次呼叫trickOrTreat()
函式以及透過trickFunction
和treatFunction
變數呼叫函式時,系統都會儲存回傳值。
Have a treat! No treats!
現在,您已瞭解函式如何傳回其他函式。您也可以將函式做為引數傳遞至其他函式。您可能會想為 trickOrTreat()
函式提供部分自訂行為,讓其執行傳回兩個字串之一以外的操作。如果函式將另一個函式做為引數,則您每次呼叫函數時,都可以傳入一個不同的函式。
將函式做為引數傳遞至其他函式
歡慶萬聖節的時候,部分地區會用零錢取代糖果,或者同時為孩子們準備零錢與糖果。您必須修改 trickOrTreat()
函式,以便將函式代表的額外 treat (點心) 做為引數來提供。
trickOrTreat()
用作參數的函式也必須能接收本身的參數。宣告函式類型時,參數不會加上標籤。您只需指定每個參數的資料類型,並以半形逗號隔開。相關語法如下圖所示:
您為接收參數的函式編寫 lambda 運算式時,系統會按照參數的出現順序為參數命名。參數名稱會列在左大括號後面,各名稱以半形逗號分隔。箭頭 (->
) 可將參數名稱與函式主體隔開。相關語法如下圖所示:
如要以參數的形式接收函式,請更新 trickOrTreat()
函式:
- 在
isTrick
參數之後,新增類型(Int) -> String
的extraTreat
參數。
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
- 在
else
區塊中,於return
陳述式之前呼叫println()
,然後傳入extraTreat()
函式的呼叫。將5
傳遞到extraTreat()
的呼叫中。
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
if (isTrick) {
return trick
} else {
println(extraTreat(5))
return treat
}
}
- 現在,當您呼叫
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()
}
- 在
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()
}
- 在
cupcake()
函式中,移除quantity
參數和->
符號。這兩個內容並未使用,因此可以略過。
val cupcake: (Int) -> String = {
"Have a cupcake!"
}
- 將呼叫更新為
trickOrTreat()
函式。如果是第一次呼叫,當isTrick
為false
時,請傳入coins()
函式。如果是第二次呼叫,當isTrick
為true
時,請傳入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()
}
- 執行程式碼。只有在
isTrick
參數設為false
引數時才會呼叫extraTreat()
函式,因此輸出結果包含 5 個 quarter,但不包括 cupcake。
5 quarters Have a treat! No treats!
可為空值函式類型
如同其他資料類型,函式類型可宣告為「空值」。在這些情況下,變數可能包含函式,也可能是 null
。
如要將函式宣告為可為空值,請用括號括住函式類型,然後在右括號外加上 ?
符號。舉例來說,如要將 () -> String
類型設為空值,請將其宣告為 (() -> String)?
類型。相關語法如下圖所示:
將 extraTreat
參數設為空值,這樣您就不必在每次呼叫 trickOrTreat()
函式時提供 extraTreat()
函式:
- 將
extraTreat
參數的類型變更為(() -> String)?
。
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
- 修改
extraTreat()
函式的呼叫,以使用if
陳述式只呼叫非空值的函式。trickOrTreat()
函式現在應如下列程式碼片段所示:
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
if (isTrick) {
return trick
} else {
if (extraTreat != null) {
println(extraTreat(5))
}
return treat
}
}
- 移除
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()
}
- 執行程式碼。輸出結果保持不變。現在您可以將函式類型宣告為可為空值,不必再將函式傳入
extraTreat
參數。
5 quarters Have a treat! No treats!
5. 使用簡短語法編寫 lambda 運算式
Lambda 運算式提供多種方式,可讓程式碼更簡潔。在本節中,您可以瞭解其中幾種方式,因為您遇到和編寫的 lambda 運算式大多都是以簡短語法寫成。
省略參數名稱
先前編寫 coins()
函式時,您已為函式的 Int
參數明確宣告名稱 quantity
。不過,如 cupcake()
函式所示,您可以完全省略參數名稱。如果函式只有一個參數,且您未提供名稱,Kotlin 會以隱含形式指定 it
名稱,因此您可以省略參數名稱和 ->
符號,讓 lambda 運算式更加簡潔。相關語法如下圖所示:
如要為參數使用簡短語法,請更新 coins()
函式:
- 在
coins()
函式中,移除quantity
參數名稱和->
符號。
val coins: (Int) -> String = {
"$quantity quarters"
}
- 變更
"$quantity quarters"
字串範本,以使用$it
參照單一參數。
val coins: (Int) -> String = {
"$it quarters"
}
- 執行程式碼。Kotlin 可辨識
Int
參數的it
參數名稱,並且仍顯示 quarter 的數量。
5 quarters Have a treat! No treats!
將 lambda 運算式直接傳遞至函式
coins()
函式目前只能在同一處中使用。如果您只需將 lambda 運算式直接傳入 trickOrTreat()
函式,而不必先建立變數,該怎麼做?
Lambda 運算式只是函式常值,就像 0
是整數常值,"Hello"
則是字串常值。您可以將 lambda 運算式直接傳遞至函式呼叫。相關語法如下圖所示:
請修改程式碼,以便移除 coins
變數:
- 移動 lambda 運算式,讓該運算式直接傳入對
trickOrTreat()
函式的呼叫。您也可以將 lambda 運算式串連為單行。
fun main() {
val coins: (Int) -> String = {
"$it quarters"
}
val treatFunction = trickOrTreat(false, { "$it quarters" })
val trickFunction = trickOrTreat(true, null)
treatFunction()
trickFunction()
}
- 移除
coins
變數,因為它無法再使用。
fun main() {
val treatFunction = trickOrTreat(false, { "$it quarters" })
val trickFunction = trickOrTreat(true, null)
treatFunction()
trickFunction()
}
- 執行程式碼。該程式碼仍會如預期編譯和執行。
5 quarters Have a treat! No treats!
使用結尾的 lambda 語法
當函式類型為函式的最後一個參數時,您可以使用另一個簡短選項來編寫 lambda。如此一來,您便可將 lambda 運算式放在右括號後面,從而呼叫函式。相關語法如下圖所示:
這會讓程式碼更容易閱讀,因為該語法會分隔 lambda 運算式與其他參數,但不會改變程式碼的用途。
如要使用結尾的 lambda 語法,請更新程式碼:
- 在
treatFunction
變數中,將 lambda 運算式{"$it quarters"}
移至trickOrTreat()
呼叫的右括號後面。
val treatFunction = trickOrTreat(false) { "$it quarters" }
- 執行程式碼。一切都還能正常運作!
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
迴圈類似。相關語法如下圖所示:
您可以使用 repeat()
函式多次呼叫 trickFunction()
函式,而非只能呼叫一次。
如要查看 repeat()
函式的實際使用狀況,請更新「不給糖就搗蛋」程式碼:
- 在
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) {
}
}
- 將
treatFunction()
函式的呼叫移至repeat()
函式的 lambda 運算式中。
fun main() {
val treatFunction = trickOrTreat(false) { "$it quarters" }
val trickFunction = trickOrTreat(true, null)
repeat(4) {
treatFunction()
}
trickFunction()
}
- 執行程式碼。
"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
迴圈類似。