什麼是匿名函數(anonymous-functions)?

定義不命名的函數,稱作匿名函數。

有了匿名函數,就能根據需求制定特殊規格,輕鬆客製化標準程式庫裡的內建函數。

例如,Kotlin標準程式庫裡有個 count 函數。應用在字串上,便能返回字串的字元個數,如下:

val numLetters = "Mississippi".count()
print(numLetters)
// 11

上述的程式碼呼叫 count() 時,使用了 點(.) 語法。只要該函數在某個資料類型的定義裡,就可以利用此語法呼叫。

承上例,如果計算字數時,我們只需要計算 s 的數量呢? Kotlin標準程式庫允許為 count 函數提供規則,以決定是否只統計某個字元。如下,把一個匿名函數作為參數傳遞給 count 函數:

val numLetters = "Mississippi".count({letter -> letter == 's'})
print(numLetters)
//4

定義函數就是把運算式或語句放在一對大括弧中。

函數類型(Function Type)

匿名函數可以當作變數值指派給函數類型變數。然後,正如其他變數一般,匿名函數就能在程式碼之間傳遞了。

val greetingFunction: () -> String = {
val currentYear = 2020
"Welcome to SimVillage, Mayor! (copyright $currentYear)"
}
println(greetingFunction())
//Welcome to SimVillage, Mayor! (copyright 2020)

:()->String表示變數儲存的是哪種類型的函數。它告訴編譯器,任何不需要參數(以小括弧表示)、能返回 String的函數,都可以指派。

如同變數的類型定義一般,無論匿名函數是指派給變數,還是作為引數傳遞,編譯器都會進行檢查,以確保其類型滿足要求。

隱式返回

上例中的匿名函式中並無 return關鍵字,和具名函數不同,除了極少數的情況之下,匿名函數不需要 return關鍵字返回資料,匿名函數會隱式或自動回傳函數本體最後一行。

函數參數

和具名函數一樣,匿名函數可以不帶參數,或者帶一個或多個任何類型的參數。需要參數時,參數的類型放在匿名函數的類型定義,參數名稱則置於函數定義。

val greetingFunction: (String) -> String = {playerName->
val currentYear = 2020
"Welcome to SimVillage, $playerName! (copyright $currentYear)"
}
println(greetingFunction("Guyal"))
//Welcome to SimVillage, Guyal! (copyright 2020)

現在匿名函數有一個String類型的參數。具體的寫法如下:

在匿名函數本體內,左大括弧的後面,寫上String 的參數名稱,後面再加上一個箭頭符號。

val greetingFunction:(String) -> String = {playerName->

it 關鍵字

定義只有一個參數的匿名函數時,可以使用 it 關鍵字表示參數名稱。上例可改為:

val greetingFunction: (String) -> String = {
val currentYear = 2020
"Welcome to SimVillage, $it! (copyright $currentYear)"
}
println(greetingFunction("Guyal"))

多個參數

it 關鍵字只適用於一個參數的情況。匿名函數支援多個參數,如果有多個參數,就必須使用具名引數。

val greetingFunction: (String, Int) -> String = {playerName, numBuildings ->
val currentYear = 2020
println("Add $numBuildings houses")
"Welcome to SimVillage, $playerName! (copyright $currentYear)"
}
println(greetingFunction("Guyal", 2))

val greetingFunction: (String, Int) -> String = {playerName, numBuildings ->

類型推斷(Type Inference)

函數類型也適用於Kotlin的類型推斷規則:定義一個變數時,如果已把匿名函數作為變數值指派給它,就不需要明確指名變數類型。

val greetingFunction: () -> String = {
val currentYear = 2020
"Welcome to SimVillage, Mayor! (copyright $currentYear)"
}
//可改為
val greetingFunction = {
val currentYear = 2020
"Welcome to SimVillage, Mayor! (copyright $currentYear)"
}

類型推斷也支援帶參數的匿名函數,但為了協助編譯器更準確地推斷變數類型,匿名函數必須要有參數名稱與參數類型。

val greetingFunction = {playerName:String, numBuildings:Int ->
val currentYear = 2020
println("Add $numBuildings houses")
"Welcome to SimVillage, $playerName! (copyright $currentYear)"
}
println(greetingFunction("Guyal", 2))

定義參數是函數的函數

接下來將把匿名函數改稱 lambda ,其定義則改稱為 lambda運算式,返回資料改稱為 lambda結果

例:新增一名為runSimulation 的函數,其中帶有兩個參數: String 以及 lambda運算式

fun runSimulation(playerName: String, greetingFunction: (String, Int) -> String) {
val numBuildings = (1 .. 3).shuffled().last() //Randomly selects 1,2 or 3
println(greetingFunction(playerName, numBuildings))
}

如果一個函數的lambda參數排在最後,或者是唯一的參數,那麼便可以省略括住lambda引號的一對小括號。

例如:

//先前的程式碼
"Mississippi".count({ it == 's'})
//可以簡寫成
"Mississippi".count{ it == 's' }

函數內聯(Function Inling)

lambda 允許我們更靈活地編寫應用程式。然而,靈活是要付出代價的。

在 JVM 上,定義的 lambda 是以物件實例的形式存在。JVM 會為所有與 lambda 打交道的變數分配記憶體,於是產生記憶體開銷。更糟的是,lambda 的記憶體開銷會帶來嚴重的效能問題。顯然,應當避免這類問題。

Kotlin 提供了一種叫做內聯的最佳化機制,有了內聯, JVM 就不需要使用 lambda 物件實例,因而避免變數記憶體的分配。

內聯關鍵字: inline,加上inline關鍵字之後,何處需要 lambda ,編譯器就會將函數本體複製貼上到那裡。

使用 lambda 的遞迴函數就無法內聯,因為內聯遞迴函數會讓複製貼上函數本體的行為形成無窮迴圈。

函數參照

函數參照可以把一個具名函數,轉換成一個引數。凡是出現 lambda 運算式的地方,都允許使用函數參照。

若想獲得函數參照,請使用 :: 運算子,後面跟著待引用的函數名稱。

如下例:::printConstructionCost

runSimulation("Guyal", ::printConstructionCost){playerName, numBuildings ->
val currentYear = 2020
println("Add $numBuildings houses")
"Welcome to SimVillage, $playerName! (copyright $currentYear)"
}

函數類型作為返回類型

如同其他資料類型一般,函數類型也是有效的返回類型,也就是說,可以定義一個能返回函數的函數。

fun configureGreetingFunction(): (String) -> String {
val structureType = "hospitals"
var numBuilding = 5
return { playerName:String ->
val currentYear = 2020
numBuilding += 1
println("Add $numBuilding $structureType")
"Welcome to SimVillage, $playerName! (copyright $currentYear)"
}
}

可以把 configureGreetingFunction 函數視為 「函數工廠」,亦即配置產生函數的函數。

能接受函數(以函數引數傳入)或者返回函數的函數又名高階函數(Higher-Order Functions)

高階函數廣泛應用於函數式程式設計這類程式設計範式中。

Kotlin 中的lambda就是閉包(Closure)

在Kotlin中,匿名函數能夠修改與使用定義在自己作用域之外的變數。換句話說,匿名函數允許引用定義自身的函數裡的變數。

例:

var sum = 0
val list = listOf(1,2,3,4,5)
list.filter{ it > 1}.forEach{
sum += it
}
print(sum)
//14

lambda 與匿名內部類別

為什麼要在程式裡使用函數類型呢?原因是,函數類型能讓開發者少寫一些模組化程式碼,設計出更靈活的程式。

Java 8 支援物件導向程式設計和 lambda 運算式,但不支援將函數作為參數,傳遞給另一個函數或變數。不過,Java的替代方案是匿名內部類別 — 定義於類別中,用來實作某個方法的無名類別。

Java 8 中的 lambda宣告需要先定義介面,而在Kotlin中,不需要而外在定義一個抽象方法。

相較於手動定義內部類別去實作一個簡單方法,透過結合這種更為簡略的語法和隱式返回、it關鍵字以及閉包行為,便提升程式碼的品質。

小記

採用 lambda 運算式,使用上就跟一般的變數無異,可以指定給變數,也可以作為引數帶入函數內,也可以作為函數的回傳值。Kotlin 也會針對 lambda 運算式 做類型推斷。

帶入lambda運算式的參數若只有一個,可以在 lambda 運算式中使用 it 來代替。

但是,使用 lambda 需要注意記憶體的消耗,在 JVM 上,定義的 lambda 是以物件實例的形式存在。JVM 會為所有與 lambda 打交道的變數分配記憶體,於是產生記憶體開銷。使用 inline 即可解決此問題,不過不是每一個 lambda 都能加上 inline 使用上需特別注意。

參考

KotlinLang.org: Higher-Order Functions and Lambdas

Kotlin Programming — The Big Nerd Ranch Guide — Ch5匿名函數與函數類型

匿名函數與lambda運算式:Ref

--

--

Andy Lu
Andy Lu

Written by Andy Lu

Android/Flutter developer, Kotlin Expert, like to learn and share.

No responses yet