Lambda表達(dá)式和高階函數(shù)
本文收錄于: https://github.com/mengdd/KotlinTutorials
在Kotlin中函數(shù)是第一公民, 不像Java一樣, 函數(shù)一定需要寫在某個類里面, Kotlin中的函數(shù)也可以直接寫在文件里.
Lambda表達(dá)式并不是什么新東西, Java 8就有了.
它的存在主要是為了讓代碼更加簡潔.
高階函數(shù)呢, 基本概念也很簡單, 就是函數(shù), 同樣也可以像其他類型的對象一樣, 作為一個函數(shù)的參數(shù)和返回值.
Lambda表達(dá)式
lambda表達(dá)式和匿名函數(shù)都是函數(shù)字面值(function literals), 它們沒有提前聲明, 直接作為表達(dá)式傳入.
Lambda表達(dá)式的語法
lambda表達(dá)式的構(gòu)成:
{ 參數(shù)列表 -> 函數(shù)體 }
其中參數(shù)類型是可以省略的.
如果函數(shù)體的返回值不是Unit, 那么最后一個表達(dá)式會被當(dāng)做返回值.
舉例: setOnClickListener
對Lambda表達(dá)式的應(yīng)用很大程度上要感謝現(xiàn)代化的IDE, 比如一個簡單的給button設(shè)置listener的代碼,
最開始, 作為一個把Kotlin當(dāng)Java 7用的人, 可能會寫成這樣:
button.setOnClickListener(object : View.OnClickListener{
override fun onClick(v: View?) {
}
})
這里利用了object關(guān)鍵字, 聲明了一個匿名類的對象.
但是IDE看到這個代碼就不那么開心了, 出現(xiàn)了一條黃色的下劃線, Alt + Enter, "Convert to lambda", 再Enter確認(rèn), 就變成了這樣:
button.setOnClickListener { }
為什么可以這樣干呢, 這是因為View的OnClickListener接口是一種特殊類型的接口: 只有一個方法.
這種只擁有一個方法的接口被稱作是函數(shù)式接口(functional interface), 也被叫做單抽象方法類型, 即SAM, (single abstract method).
PS: 一個錯誤的示范:
如果你不幸一開始把代碼寫成了這樣:
button.setOnClickListener{object : View.OnClickListener {
override fun onClick(v: View?) {
}
}}
注意這里與上面例子的區(qū)別僅僅是把小括號()變成了大括號{}.
程序沒有報錯, 但按鈕點擊的時候應(yīng)該是執(zhí)行不到你想要的代碼了.
IDE仍然會提示你簡化, 簡化后變成了這樣:
button.setOnClickListener{ View.OnClickListener { } }
也就是說每次按鈕點擊, 你做的事情都是創(chuàng)建了一個匿名對象.
為什么會犯這種錯誤, 而IDE又不提示呢, 這是因為lambda的慣例允許這樣的寫法, 這么寫雖然邏輯上有問題, 但是語法上是沒有錯誤的.
下面請看都有什么慣例呢.
lambda慣例
- 如果函數(shù)的最后一個參數(shù)接收函數(shù), 傳入的lambda表達(dá)式可以寫在圓括號外面. (這種語法稱為trailing lambda).
在這種情況下, 如果lambda是唯一的參數(shù), 那么圓括號可以省略. (上面的錯誤例子就是因為符合了這個慣例, 所以語法上沒有錯誤). - 如果lambda只有一個參數(shù), 可以不聲明直接用, 隱式名字
it. - lambda表達(dá)式的返回值: 可以顯式
return, 如果不顯式返回, 默認(rèn)返回最后一個表達(dá)式的值. - 沒有用到的參數(shù)可以用下劃線
_表示.
匿名函數(shù)
lambda表達(dá)式并不能顯式指定返回類型, 大多數(shù)情況下可能用不上這一點, 因為往往返回類型都是可以被自動推斷出來的.
但是如果你真的需要顯式指定, 你可以用匿名函數(shù).
fun(x: Int, y: Int): Int = x + y
匿名函數(shù)看起來和普通的函數(shù)聲明很像, 只是它沒有名字.
和普通函數(shù)不同的是, 如果參數(shù)類型可以從上下文推斷出來, 那么可以省略不寫參數(shù)類型:
ints.filter(fun(item) = item > 0)
匿名函數(shù)的返回類型推斷和正常函數(shù)一樣.
匿名函數(shù)與Lambda的區(qū)別:
- 匿名函數(shù)的參數(shù)永遠(yuǎn)是在圓括號內(nèi)傳遞的, 把函數(shù)參數(shù)放在圓括號外的簡化語法只適用于lambda表達(dá)式.
- 不帶標(biāo)簽的return語句: 在lambda中, 返回外層的函數(shù), 在匿名函數(shù)中, 返回本函數(shù).
高階函數(shù)
高階函數(shù)(Higher-Order Functions): 把函數(shù)作為參數(shù)或返回值的函數(shù).
函數(shù)類型
函數(shù)類型(Function types): 函數(shù)根據(jù)參數(shù)和返回值, 可以歸類到一個函數(shù)類型.
函數(shù)類型的寫法
函數(shù)類型的基本寫法: 括號中的參數(shù)列表和一個返回值. 比如(A, B) -> C就是一個函數(shù)類型, 這種類型的函數(shù)接受A和B兩種類型的參數(shù), 返回一個類型C的返回值.
- 參數(shù)列表可以為空, 比如
() -> A. - 返回值為
Unit時不能省略. - 函數(shù)類型還可以寫接收器(receiver)類型:
A.(B) -> C. 表示這種函數(shù)是在A類型上調(diào)用的. -
suspend關(guān)鍵字表示一種特殊的函數(shù)類型, 所以如果有,suspend修飾符也要出現(xiàn). 比如suspend () -> Unit. - 也可以加上參數(shù)名, 來表達(dá)參數(shù)的意義. 如
(x: Int, y: Int) -> Point.
幾點比較特殊的:
- 函數(shù)類型也有可空類型, 加括號和
?. 比如((Int, Int) -> Int)?. - 函數(shù)類型可以是高階的, 用括號組合, 如:
(Int) -> ((Int) -> Unit). - 箭頭是按照右結(jié)合的原則, 即運算按照從右向左的順序, 所以
(Int) -> (Int) -> Unit和(Int) -> ((Int) -> Unit)是一個意思.
可以給函數(shù)類型一個類型別名, 比如:
typealias ClickHandler = (Button, ClickEvent) -> Unit
實例化函數(shù)類型
實例化函數(shù)類型有好幾種方法:
- 用函數(shù)字面值(function literal)的代碼塊: lambda表達(dá)式, 匿名函數(shù).
- 使用已有的聲明引用: 函數(shù), 屬性, 構(gòu)造函數(shù).
::的用法參見Callable Reference. - 函數(shù)類型還可以當(dāng)做接口被類實現(xiàn), 那么創(chuàng)建這個類的實例就是創(chuàng)建了函數(shù)類型的實例.
調(diào)用函數(shù)類型的實例
函數(shù)類型聲明了, 也實例化了, 怎么調(diào)用呢?
可以用invoke操作符, 也可以直接用名稱調(diào)用.
如果有接受者類型(receiver), 那么接受者對象需要作為第一個參數(shù).
另一種方式也可以將接受者對象放在點(.)前面, 像擴(kuò)展函數(shù)一樣調(diào)用.
val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus
println(stringPlus.invoke("<-", "->"))
println(stringPlus("Hello, ", "world!"))
println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3)) // extension-like call