函數和Lambda表達式
Kotlin融合了面向過程語言和面向對象語言的特征,相比于Java,它增加了對函數式程式設計的支援,支援定義函數、調用函數。相比于C語言,Kotlin支援局部函數(Lambda表達式的基礎)。
6.1 函數入門
6.1.1 定義和調用函數
定義函數的文法格式如下:
fun 函數名 (形參清單) [: 傳回值類型] {
//函數體
}
// 函數的聲明必須使用fun關鍵字
// 形參清單 “形參名: 參數類型”
// 舉例:
fun max(x: Int, y: Int): Int {
val res = if(x > y) x else y
return res
}
6.1.2 函數傳回值和Unit
如果希望一個函數沒有傳回值,可以通過以下兩種方式實作:
- 省略“: 傳回值類型”部分
- 使用“: Unit”指定傳回Unit -> 代表沒有傳回值
fun func1(str: String) {
println(str)
}
fun func2(str: String): Unit {
println(str)
}
6.1.3 遞歸函數
在函數體内調用該函數本身。
…
6.1.4 單表達式函數
如果函數隻是傳回單個表達式,那麼可以省略花括号并在函數名後用等号指定函數體。這種形式的函數被稱為單表達式函數。
fun area1(x: Int, y: Int): Int {
return x*y;
}
fun area2(x: Int, y: Int): Int = x*y
//函數area1和函數area2等效,其中area2是單表達式函數
6.2 函數的形參
相比于Java,Kotlin形參的功能更加豐富且靈活。
6.2.1 命名參數
Kotlin中參數名是有意義的。
Kotlin除了可以按照傳統的方式——根據參數的位置(順序)來傳入參數之外,還可以使用命名參數來傳參(此時可以不按順序傳參)。
還可以以上二者混合使用,但此時要求位置參數必須位于命名參數之前。
fun area(width: Double, height: Double): Double {
var a = width * height
println("width=${width}, height=${height}, area=${a}")
return a
}
var res: Double
//傳統的傳參方式——位置參數,根據形參位置傳參
res = area(2.0, 5.0)
//Kotlin中支援的傳參方式——命名參數,根據形參名傳參
res = area(heigth = 5.0, width = 2.0)
//二者的混合使用
res = area(2.0, height = 5.0)
6.2.2 形參預設值
在Kotlin中,使用者可以在定義函數時為一個或多個形參指定預設值——這樣在調用函數時,就可以省略該形參值的傳入,而使用形參預設值。文法格式如下:
形參名: 形參類型 = 預設值
fun sayHi(name: String = "Seraph", msg: String = "hello !") {
println("${name}, ${msg}")
}
sayHi() //全部使用預設參數,輸出:Seraph, hello !
sayHi("Jack", "see you !") //全部使用位置參數
sayHi(name = "Leo", msg = "bye~") //全部使用命名參數
sayHi(msg = "bye~") //預設參數 + 命名參數
sayHi("Ben") // 位置參數 + 預設參數, 輸出:Ben, hello !
通常建議将帶預設值的參數定義在形參清單的最後。
6.2.3 尾遞歸函數
當函數将自身作為其執行體的最後一行代碼,且遞歸調用後沒有更多的代碼時,可以使用尾遞歸文法。
尾遞歸不能在try、catch、finally塊中使用。
尾遞歸函數需要使用tailrec修飾。
//定義計算階乘的函數
fun fact(n: Int): Int {
if(n == 1) {
return 1
} else {
return n * fact(n-1)
}
}
//使用尾遞歸文法改寫上述函數
tailrec fun factRec(n: Int, total: Int = 1): Int = if(n == 1) total else factRec(n-1, total * n)
編譯器會将尾遞歸優化成一個快速且高效的基于循環的版本,這樣可以減少對記憶體的消耗。
6.2.4 個數可變的形參
Kotlin中,我們可以通過在形參名前添加
vararg
修飾符,以定義個數可變的形參,進而為函數指定數量不定的形參。
fun test(a: Int, vararg books: String) {
//books被當做數組處理
for(book in books) {
println(book)
}
}
Kotlin允許個數可變的形參位于參數清單中的任意位置,但是要求一個函數隻能帶一個形參。
6.3 函數重載
一個kt檔案中包含了兩個或兩個以上函數名相同、形參清單不同的函數,被稱為函數的重載。
與Java類似的是,Kotlin中函數的重載也隻能通過形參清單進行區分,形參個數不同、形參類型不同都可以算作函數的重載。但是形參名不同、傳回值類型不同或修飾符不同,都不能算函數的重載。
6.4 局部函數
Kotlin支援在函數體内定義函數,這種定義在函數體内的函數别稱之為局部函數。
預設情況下,局部函數是對外部隐藏的,隻在其封閉函數内有效。其封閉函數也可以傳回局部函數,以便程式在其他作用域中使用局部函數。
fun A(type: String) {
fun B(name: String) {
println("name=${name}")
}
}
6.5 高階函數
Kotlin中函數也是一等公民,函數本身也有自己的類型——函數類型。函數類型既可以用于定義變量,也可以用于形參、傳回值。
6.5.1 使用函數類型
函數類型由:函數形參清單、-> 、傳回值類型 這三者組成。
fun foo(a: Int, name: String) -> String {
......
}
// 函數foo的函數類型為:(Int, String) -> String
fun bar() {
......
}
// 函數bar的函數類型為:() 或 () -> Unit
使用函數類型定義變量的方法如下:
var myFun: (Int, Int) -> Int
fun pow(base: Int, exponent: Int): Int {
var res = 1
for(i in 1 .. exponent) {
res *= base
}
return res
}
fun area(width: Int, height: Int): Int = width * height
myFun = ::pow //将pow函數指派給myFun
println(myFun(2, 3)) //輸出:8
myFun = ::pow //将area函數賦給myFun
println(myFun(2, 3)) //輸出:6
當直接通路一個函數的函數引用,而不是調用該函數時,需要在函數名前添加兩個冒号,而且不能在函數名後添加圓括号——一旦添加圓括号就變成了調用函數而不是通路函數引用。
6.5.2 使用函數類型作為形參
fun map(data: Int, fn: (Int) -> Int): Unit {
......
}
6.5.3 使用函數類型作為傳回值類型
fun getMathFunc(type: String): (Int) -> Int {
fun square(n: Int): Int = n * n
fun cube(n: Int): Int = n * n * n
fun factorial(n: Int): Int {
var res = 1;
for(index in 2 .. n) {
res *= index
}
return res
}
when(type) {
"square" -> return ::square
"cube" -> return ::cube
else -> return ::factorial
}
}
6.6 局部函數與Lambda表達式
Lambda表達式是現代程式設計語言争相引入的文法,它是功能更靈活的代碼塊,可以在程式中被傳遞和調用。
6.6.1 回顧局部函數
略
6.6.2 使用Lambda表達式代替局部函數
可以使用Lambda表達式來簡化局部函數。
fun getMathFunc(type: String): (Int) -> Int {
when(type) {
"square" -> return {n: Int ->
n * n
}
"cube" -> return {n: Int ->
n * n * n
}
else -> return { n: Int ->
var res = 1
for(index in 2 .. n) {
res *= index
}
res
}
}
}
Lambda表達式和局部函數差別如下:
- Lambda表達式總是被大括号括着
- 定義Lambda表達式不需要fun關鍵字,無需指定函數名
- 形參清單(如果有)在 -> 之前聲明,參數類型可以省略。
- 函數體(Lambda表達式的執行體)放在 -> 之後
- 函數的最後一個表達式自動被作為Lambda表達式的傳回值,無需使用 return 關鍵字
6.6.3 Lambda表達式的脫離
作為參數傳入的Lambda表達式可以脫離函數獨立使用。
6.7 Lambda表達式
Lambda表達式文法如下:
{ (形參清單) ->
零到多條可執行語句
}
6.7.1 調用Lambda表達式
Lambda表達式可以被指派給變量或者直接調用。
// 定義一個Lambda表達式,并将它指派給square
var square = { n: Int ->
n * n
}
println(square(5)) //輸出:25
// 定義一個Lambda表達式,并在其後添加圓括号來調用該表達式,并将結果賦給res
var res = { base: Int, exponent: Int ->
var result = 1
for(i in 1 .. exponent) {
result *= base
}
result
}(4, 3)
println(res) //輸出64
6.7.2 利用上下文推斷類型
如果Kotlin根據Lambda表達式的上下文推斷出形參類型,那麼Lambda表達式就可以省略形參類型。
// 變量square的類型已經被聲明了,是以Kotlin可以推斷出Lambda表達式的形參類型
// 是以Lambda表達式可以省略形參類型
var square: (Int) -> Int = {n -> n * n}
var res = {a, b -> a + b} //非法,因為Kotlin無法推斷形參a,b的資料類型
6.7.3 省略形參名
如果隻有一個形參,那麼Kotlin允許省略Lambda表達式的形參名。當形參名被省略時,Lambda表達式用
it
來代表形參。同時, -> 也不需要了。
// 省略形參名,用it代替
var square: (Int) -> Int = {it * it}
6.7.4 調用Lambda表達式的約定
Kotlin中約定:如果函數的最後一個形參是函數類型,且你打算傳入一個Lambda表達式作為相應的參數,那麼就允許在圓括号之外指定Lambda表達式。
var list = listOf("Java", "Kotlin", "Go")
var rt = list.dropWhile(){it.length > 3} //dropWhile()方法的參數清單為: (T) -> Boolean
println(rt) //輸出: [Go]
如果Lambda表達式是函數調用的唯一參數,則調用時函數的圓括号可以省略:
6.7.5 個數可變的參數和Lambda參數
前面有提到:個數可變的形參可以定義在參數清單中的任意位置,但是如果不将它放在最後,就隻能用命名參數的形式為可變形參之後的其他形參傳值。是以建議将可變形參放在形參清單的最後。
那麼我們到底是應該将函數類型的參數放在形參清單的最後,還是将個數可變的參數放在形參清單最後呢?
答案:如果一個函數既包含個數可變的形參,也包含函數類型的形參,那麼應該将函數類型的形參放在最後。
fun <T> test(vararg name: String, transform: (String) -> T): List<T> {
......
}
6.8 匿名函數
Lambda表達式無法指定傳回值類型,且在一些特殊場景下Kotlin也無法推斷出Lambda表達式的傳回值類型。此時可以匿名函數來代替Lambda表達式。
隻要将普通函數的函數名去掉就成了匿名函數。
var test = fun(x: Int, y: Int): Int {
return x + y
}
6.9 捕獲上下文中的變量和常量
Lambda表達式、匿名函數、局部函數都可通路或修改其所在上下文中的變量和常量。
6.10 内聯函數
由于函數的調用過程會産生一定的時間和空間上的開銷,為了避免這部分開銷(即避免産生函數的調用過程),我們可以考慮通過
内聯函數
的方式,将被調用的函數或表達式”嵌入“原來的執行流中。
// 通過inline關鍵字聲明内聯函數
inline fun mFun(data: Int) {
......
}