Kotlin 高階函式與 Lambda 表示式
在 Kotlin 中函式也是一等公民
,這意味著我們定義的變數、函式引數、返回值都可以是函式型別的,可以像操作其它非函式值一樣操作函式,確實也方便了不少。對 Android 開發者而言這無疑是一個較大的變化(雖然從 Java8 開始也有了類似的操作),同時也是 Kotlin 中相對重要的知識點,值得我們深入學習。
一、高階函式
高階函式是將函式作為引數或返回值的函式。這裡有三個問題需要我們先思考:
- 如何定義一個函式的引數為函式型別?
- 如何使用這個函式型別的引數?
- 函式作為引數如何傳遞?
先看一個高階函式實現的例子:
class Calculator { fun sum(a: Int, b: Int): Int = a + b fun calculate(a: Int, b: Int, cal: (Int, Int) -> Int) { print("a + b = ${cal(a, b)}") } } fun main(args: Array<String>) { val calculator = Calculator() calculator.calculate(1, 1, calculator::sum) } // 輸出 a + b = 2
現在回答上邊的三個問題
-
cal: (Int, Int) -> Int
就定義了calculate()
函式的第三個引數為函式型別,同時該型別的函式有兩個Int
型別引數,返回值為Int
型別。所以->
左邊括號部分為函式引數型別宣告,右邊為函式的返回值。注意,如果是無參函式括號()
不能省略。 -
cal(a, b)
就是使用這個函式型別的引數,和普通函式的呼叫沒啥區別。 -
注意
main()
方法的最後一行,我們使用了calculator::sum
這種寫法,注意sum()
方法的宣告和cal
引數的型別是一致的。
同樣的原理,函式也可以作為返回值,簡單修改上邊的程式碼:
class Calculator { fun sum(a: Int, b: Int): Int = a + b fun getSum(): (Int, Int) -> Int = this::sum } fun main(args: Array<String>) { val f = Calculator().getSum() print(f(1, 1)) } // 輸出 2
二、Lambda 表示式
1、初識 Lambda 表示式
Lambda 表示式本質上是可以傳遞給其它函式來作為引數的程式碼塊。咦?這樣說 Lambda 表示式也可作為高階函式的函式型別引數的值?當然,Lambda 表示式為函數語言程式設計提供了更好的實現。先修改上邊的程式碼:
class Calculator { fun calculate(a: Int, b: Int, cal: (Int, Int) -> Int) { print("a + b = ${cal(a, b)}") } } fun main(args: Array<String>) { val calculator = Calculator() calculator.calculate(1, 1, { a, b -> a + b }) } // 輸出 a + b = 2
和第一部分中主要的不同之處是,呼叫calculate()
方法時使用了{ a, b -> a + b }
作為第三個引數的值,其實{ a, b -> a + b }
就是一個 Lambda 表示式,這裡省略了它的引數型別,完整的如下:
{ a: Int, b: Int -> a + b }
通過例子我們可以看出 Lambda 表示式的書寫規則如下:
-
Lambda 表示式總是被花括號
{}
包裹著 -
->
左邊為引數定義部分,多個引數用逗號,
間隔,類似函式的引數宣告,但 Lambda 表示式的引數型別可以省略(編譯器可以推斷型別)。 -
->
右邊為 Lambda 表示式要執行的業務邏輯,類似於函式體
2、Lambda 表示式的一些特性
- 如果函式的最後一個引數接受函式,那麼傳入的 Lambda 表示式可以放在圓括號之外:
fun calculate(a: Int, b: Int, cal: (Int, Int) -> Int) { print("a + b = ${cal(a, b)}") } fun main(args: Array<String>) { calculate(1, 1) { a, b -> a + b } }
-
如果 Lambda 表示式只有一個引數,並且編譯器自己可以識別出簽名,也可以不用宣告唯一的引數並忽略
->
, 該引數會隱式宣告為it
:
fun calculate(a: Int, cal: (Int) -> Int) { print(cal(a)) } fun main(args: Array<String>) { calculate(2) { it * it } } // 輸入 4
-
如果 Lambda 表示式沒有引數,也可以忽略
->
, 並且不會隱式宣告引數it
,可參考下一點的例子。 -
如果 Lambda 表示式是呼叫時唯一的引數,那麼圓括號也可以省略:
fun myPrint(p: () -> Unit) { p() } fun main(args: Array<String>) { myPrint { print("timestamp is ${System.currentTimeMillis()}") } }
- Lambda 表示式將隱式返回最後一個表示式的值,但可以使用限定的返回語法,即通過ofollow,noindex">標籤 顯式返回一個值:
fun calculate(a: Int, cal: (Int) -> Int) { print(cal(a)) } fun main(args: Array<String>) { calculate(2) { it * it } calculate(2) { return@calculate it * it } }
- 從 Kotlin1.1 起,如果 Lambda 表示式的引數未使用,則可以用下劃線代替其名稱:
fun calculate(a: Int, b: Int, cal: (Int, Int) -> Int) { print(cal(a, b)) } fun main(args: Array<String>) { calculate(1, 2) { _, b -> println("a 引數未使用") b * b } }
-
從 Kotlin1.1 起,Lambda 表示式引數支援解構宣告語法
,我們通過 Kotlin 內建的
forEach()
方法遍歷map
來測試:
fun main(args: Array<String>) { val map = mapOf(1 to 1, 2 to 2, 3 to 3) map.forEach { println("${it.key} to ${it.value}") } // 顯示宣告it引數的型別 map.forEach { entry: Map.Entry<Int, Int> -> println("${entry.key} to ${entry.value}") } // 將Map.Entry型別的it引數解構 map.forEach { (k, v) -> println("$k to $v") } }
三、匿名函式
其實 Lambda 表示式有一個問題,就是無法顯示的指定其返回值的型別,雖然可以自動推斷出返回值型別,如果確實需要顯式指定返回值型別,可以匿名函式,和普通的函式類似,只是省略了函式名:
fun(a: Int, b: Int): Int { return a + b } // 或者 fun(a: Int, b: Int): Int = a + b
用法和 Lambda 表示式也有差別的:
fun calculate(a: Int, b: Int, cal: (Int, Int) -> Int) { println("a + b = ${cal(a, b)}") } val sum = fun(a: Int, b: Int): Int = a + b fun main(args: Array<String>) { calculate(1, 1, sum) calculate(2, 2, fun(a: Int, b: Int): Int = a + b) }
四、訪問閉包
Lambda 表示式或者匿名函式(以及區域性函式 和物件表示式 ) 可以訪問其閉包 ,即在外部作用域中宣告的變數。 與 Java 不同的是可以修改閉包中捕獲的變數:
fun calculate(a: Int, b: Int, cal: (Int, Int) -> String) { print(cal(a, b)) } fun main(args: Array<String>) { var str: String calculate(1, 1) { a, b -> str = "a + b = " "$str${a + b}" } } // 輸出 a + b = 2