什麼是匿名函數(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