1. 簡介
本程式碼研究室將介紹函式類型、函式類型的使用方式,以及 lambda 運算式特有的語法。
在 Kotlin 中,系統會將函式視為一級結構。這表示函式可視為資料類型。您可將函式儲存在變數中,將函式做為引數傳遞至其他函式,以及從其他函式傳回函式。
如同其他可使用常值表示的資料類型 (例如 10
值的 Int
類型和 "Hello"
值的 String
類型),您也可以宣告函式常值,這通常稱為 lambda 運算式 (簡稱 lambda)。Android 開發作業會廣泛使用 lambda 運算式,而這類運算式在 Kotlin 程式設計中也較為普遍。
必要條件
- 熟悉 Kotlin 程式設計,包含函式、
if/else
陳述式及是否可為空值
課程內容
- 如何使用 lambda 語法定義函式。
- 如何將函式儲存在變數中。
- 如何將函式做為引數傳遞至其他函式。
- 如何從其他函式傳回函式。
- 如何使用空值函式類型。
- 如何讓 lambda 運算式更簡潔。
- 說明高階函式。
- 如何使用
repeat()
函式。
軟硬體需求
- 可使用 Kotlin Playground 的網路瀏覽器。
2. 觀看程式設計示範影片 (可略過)
如果您想觀看課程老師示範完成此程式碼研究室,請觀看以下影片。
建議您在全螢幕模式下觀看影片 (點選影片右下角的 圖示),以便清楚看見 Android Studio 和程式碼。
您可以跳過這個步驟,也可以不觀看這段影片,立即開始進行程式碼研究室的操作步驟。
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)
}
- 呼叫
trickFunction()
,然後在下一行呼叫treatFunction()
。
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
參數數量命名,並使用->
運算子將其與函式主體隔開。您現可將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"
字串應列印 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
迴圈類似。