Kotlin快速入門(五)——函式和Lambda表示式
函式和Lambda表示式
Kotlin對Java的存粹的面向物件進行了彌補,增加了函數語言程式設計的支援,提高了程式設計的靈活性。對於Java程式員來講,Kotlin的函式最需要花精力來掌握的內容。
1. 函式入門
-
定義和呼叫函式
必須使用
fun
關鍵字宣告。fun main(args: Array<String>) { println("3和5中較大的是${max(3, 5)}") } fun max(a: Int, b: Int): Int { return if (a > b) a else b }
-
函式返回值和
Unit
如果沒有返回值可以省略,或者使用
:Unit
fun main(args: Array<String>) { sayHi("JamFF") showMsg("msg", 2) } fun sayHi(name: String) { println("你好,$name") } fun showMsg(msg: String, count: Int): Unit { for (i in 1..count) { println("$i: $msg") } }
-
遞迴函式
已知數列:f(0) = 1, f(1) = 4, f(n+2) = 2 * f(n+1) + f(n),其中n是大於0的整數,求f(4)的值。
fun main(args: Array<String>) { println("f(4) = ${f(4)}") } fun f(num: Int): Int { return if (num == 0) 1 else if (num == 1) 4 else 2 * f(num - 1) + f(num - 2) }
-
單表示式函式
函式只是返回單個表示式,可以省略花括號並在等號後指定函式體即可。這種方式被稱為單表示式函式。
fun main(args: Array<String>) { println(area(2.0, 3.0)) } fun area(x: Double, y: Double): Double = x * y
編譯器可以推斷出函式返回值型別,還可以省略返回值型別。
fun area(x: Double, y: Double) = x * y
2. 函式的形參
-
命名引數(具名引數)
fun main(args: Array<String>) { println(area(width = 2.0, height = 3.0)) println(area(height = 3.0, width = 2.0)) println(area(2.0, height = 3.0)) // 報錯,命名引數必須在位置引數後面 println(area(width = 2.0, 3.0)) // 報錯,位置引數是width,後面的命名引數還是width println(area(3.0, width = 2.0)) } fun area(width: Double, height: Double): Double = width * height
-
形參預設值
通過形參預設值,可以減少函式過載數量 。
fun main(args: Array<String>) { showMsg() showMsg("Tony", "Hello Java") showMsg("Tony") showMsg(msg = "Hello Java") } fun showMsg(name: String = "JamFF", msg: String = "Hello Kotlin") { println("$name: $msg") }
通常建議將帶預設值的引數定義在形參列表的最後 。
fun main(args: Array<String>) { printTriangle('*') } // 列印三角形 fun printTriangle(char: Char, height: Int = 5) { for (i in 1..height) { for (j in 0 until height - 1) { print(" ") } for (j in 0 until 2 * i - 1) { print(char) } println() } }
-
尾遞迴函式
當函式將呼叫自身作為它執行的最後一行程式碼,且遞迴呼叫後沒有更多的程式碼時可以使用尾遞迴語法。
另外,尾遞迴不能在異常處理的try、catch、finall塊中使用。
尾遞迴函式需要使用
tailrec
修飾。fun main(args: Array<String>) { println(fact(4))// 輸出24 println(facRec(4))// 輸出24 } // 普通遞迴,計算階乘。這裡的返回值不能省略,使用遞迴else返回值不明確 fun fact(n: Int): Int = if (n == 1) 1 else n * fact(n - 1) // 尾遞迴優化,計算階乘。這裡的返回值不能省略,使用遞迴else返回值不明確 tailrec fun facRec(n: Int, total: Int = 1): Int = if (n == 1) total else facRec(n - 1, total * n)
與普通遞迴相比,編譯器會進行優化,減少記憶體消耗 。
-
個數可變的形參(可變引數)
在形參的型別前新增
vararg
修飾,表示該形參可以接受多個引數值,多個引數值被當作陣列傳入。fun main(args: Array<String>) { test(5, "JamFF", "Jason") } fun test(a: Int, vararg names: String) { for (name in names) { println(name) } println(a) }
可變引數可以處於引數列表的任意位置(不要求是最後一個引數),但一個函式最多隻能帶一個可變引數 ,並且如果給可變引數後面的引數傳參,必須使用命名引數 。
fun main(args: Array<String>) { test("JamFF", "Jason", num = 5) } fun test(vararg names: String, num: Int) { for (name in names) { println(name) } println(num) }
如果已經有一個數組,希望把陣列傳入可變引數,可以在陣列引數前新增
*
運算子。fun main(args: Array<String>) { val arr = arrayOf("JamFF", "Jason") test(*arr, num = 5) }
3. 函式過載
與Java一致。
如果被過載的函式包含可變引數,Kotlin會盡量執行最精確的匹配。
fun main(args: Array<String>) { test()// 可變引數 test("JamFF", "Jason")// 可變引數 test("JamFF")// 一個引數 } fun test(msg: String) { println("只含有一個字串的test函式 $msg") } fun test(vararg names: String) { println("可變引數 ${names.contentToString()}") }
大部分情況下不推薦過載可變引數的函式 ,沒有意義並且容易導致錯誤。
4. 區域性函式
Kotlin支援在函式體內部定義函式,這種函式被稱為區域性函式。
預設情況下,區域性函式對外部是隱藏的,區域性函式只能在其封閉(enclosing)函式內有效,其封閉函式也可以返回區域性函式,以便程式在其他作用域中使用區域性函式。
fun main(args: Array<String>) { println(getMathFunc("square", 3)) println(getMathFunc("cube", 3)) println(getMathFunc("factorial", 3)) } fun getMathFunc(type: String, nn: Int): Int { // 平方的區域性函式 fun square(n: Int): Int { return n * n } // 立方的區域性函式 fun cube(n: Int): Int { return n * n * n } // 階乘的區域性函式 fun factorial(n: Int): Int { var result = 1 for (index in 2..n) { result *= index } return result } // 使用when表示式,簡化多個return return when (type) { "square" -> square(nn) "cube" -> cube(nn) else -> factorial(nn) } }
如果程式使用變數儲存了封閉函式的返回值 ,那麼這個區域性函式的作用域就會被擴大,和全域性函式是一樣的。
5. 高階函式
Kotlin的函式也是一等公民。
-
使用函式型別
定義函式型別的變數
fun main(args: Array<String>) { // 定義一個變數,型別為(Int, Int) -> Int val myFun: (Int, Int) -> Int // 定義一個變數,型別為(String) val test: (String) }
賦值使用
fun main(args: Array<String>) { // 定義一個變數,型別為(Int, Int) -> Int var myFun: (Int, Int) -> Int // 定義一個變數,型別為(String) -> Unit,返回值不能省略 val test: (String) -> Unit test = ::show test("JamFF") myFun = ::pow println(myFun(2, 4))// 計算2的4次方,輸出16 myFun = ::area println(myFun(2, 4))// 計算面積,輸出8 } fun show(msg: String) { println(msg) } // 計算乘方 fun pow(base: Int, exponent: Int): Int { var result = 1 for (i in 1..exponent) { result *= base } return result } // 計算面積 fun area(width: Int, height: Int): Int { return width * height }
-
使用函式型別作為形參型別
fun main(args: Array<String>) { val data = arrayOf(1, 2, 3, 4, 5) println("原資料${data.contentToString()}") println("計算元素平方${map(data, ::square).contentToString()}") println("計算元素立方${map(data, ::cube).contentToString()}") println("計算元素階乘${map(data, ::factorial).contentToString()}") } // 平方 fun square(n: Int): Int { return n * n } // 立方 fun cube(n: Int): Int { return n * n * n } // 階乘 fun factorial(n: Int): Int { var result = 1 for (index in 2..n) { result *= index } return result } // fn是函式型別的形參 fun map(data: Array<Int>, fn: (Int) -> Int): Array<Int> { val result = Array(data.size) { 0 } for (i in data.indices) { result[i] = fn(data[i]) } return result }
-
使用函式型別作為返回值型別
fun main(args: Array<String>) { var mathFunc = getMathFunc("cube") println(mathFunc(5))// 輸出125 mathFunc = getMathFunc("square") println(mathFunc(5))// 輸出25 mathFunc = getMathFunc("other") println(mathFunc(5))// 輸出120 } // 返回值型別是函式型別 fun getMathFunc(type: String): (Int) -> Int { // 平方 fun square(n: Int): Int { return n * n } // 立方 fun cube(n: Int): Int { return n * n * n } // 階乘 fun factorial(n: Int): Int { var result = 1 for (index in 2..n) { result *= index } return result } // 返回區域性函式 return when (type) { "square" -> ::square "cube" -> ::cube else -> ::factorial } }
6. 區域性函式與Lambda表示式
如果說函式是命名的、方便複用的程式碼塊,那麼Lambda表示式則是功能更靈活的程式碼塊,它可以在程式中被傳遞和呼叫。
-
使用Lambda表示式代替區域性函式
使用Lambda表示式簡化上面的程式碼。
fun main(args: Array<String>) { var mathFunc = getMathFunc("cube") println(mathFunc(5))// 輸出125 mathFunc = getMathFunc("square") println(mathFunc(5))// 輸出25 mathFunc = getMathFunc("other") println(mathFunc(5))// 輸出120 } // 返回值型別是函式型別 fun getMathFunc(type: String): (Int) -> Int { // 返回區域性函式 return when (type) { "square" -> { n: Int -> n * n } "cube" -> { n: Int -> n * n * n } else -> { n: Int -> var result = 1 for (index in 2..n) { result *= index } result } } }
-
Lambda表示式的脫離
作為函式引數傳入的Lambda表示式可以脫離函式獨立使用。
fun main(args: Array<String>) { collectFn { a: Int -> a * a } // 如果只有一個形參,Kotlin可以省略形參名,如果省略的話,->也不需要了,用it代替形參。 collectFn { it * it }// 上面表示式的簡寫,在下面Lambda表示式會詳細說到 collectFn { it * it * it } for (i in lambdaList.indices) { println(lambdaList[i](i + 2)) } } // 定義一個List型別的變數 val lambdaList = ArrayList<(Int) -> Int>() // 返回值型別是函式型別 fun collectFn(fn: (Int) -> Int) { // 將傳入的fn(函式或Lambda表示式)新增到集合 lambdaList.add(fn) }
上面程式吧Lambda表示式作為引數傳給collectFn()
函式後,這些Lambda表示式可以脫離collectFn()
函式使用。
7. Lambda表示式
Lambda表示式的標準語法如下:
{ (形參列表) -> // 零條到多條可執行語句 }
fun -> -> return
-
呼叫Lambda表示式
可以將Lambda表示式賦值給變數或直接呼叫Lambda表示式
fun main(args: Array<String>) { // 將Lambda表示式賦值給變數 val square = { n: Int -> n * n } println(square(5))// 輸出25 // 在Lambda表示式後面新增圓括號,直接呼叫 val result = { base: Int, exponent: Int -> var result = 1 for (i in 1..exponent) { result *= base } result }(4, 3) println(result)// 輸出64 }
-
利用上下文推斷型別
完整的Lambda表示式需要定義形參型別,但是如果Kotlin可以根據Lambda表示式上下文推斷出形參型別,那麼就可以省略形參型別。
fun main(args: Array<String>) { // Lambda表示式被賦值為(Int) -> Int型別的變數,可以推斷形參型別 val square: (Int) -> Int = { n -> n * n } println(square(5))// 輸出25 // 直接傳參(4, 3),不能推斷形參型別,所以需要顯式宣告形參型別 val result = { base: Int, exponent: Int -> var result = 1 for (i in 1..exponent) { result *= base } result }(4, 3) println(result)// 輸出64 val list = listOf("Java", "Kotlin", "Go") // 因為dropWhile方法的形參是(T) -> Boolean型別,所以可以推斷形參類就是集合元素型別 // dropWhile返回一個新List,返回從第一項起,去掉滿足條件的元素(lambda返回true),直到不滿足條件的一項為止 val rt = list.dropWhile({ e -> e.length > 3 }) // Lambda可以簡化,在下面Lambda表示式會詳細說 // val rt = list.dropWhile { it.length > 3 } println(rt)// 輸出[Go] }
-
省略形參名
Lambda表示式不僅可以省略形參型別,而且如果只有一個形參 ,那麼Kotlin允許省略Lambda表示式的形參名,如果省略了,那麼->也不需要了,Lambda表示式可通過
it
來代表形參。fun main(args: Array<String>) { // 用it代表形參 val square: (Int) -> Int = { it * it } println(square(5))// 輸出25 val list = listOf("Java", "Kotlin", "Go") // 用it代表形參 val rt = list.dropWhile({ it.length > 3 }) println(rt)// 輸出[Go] }
-
呼叫Lambda表示式的約定
如果函式的最後一個引數是函式型別,並且你打算傳入一個Lambda表示式作為相應的引數,那麼就允許在圓括號之外指定Lambda表示式。
fun main(args: Array<String>) { val list = listOf("Java", "Kotlin", "Go") // 最後一個引數是Lambda表示式,可以將表示式寫在圓括號外面 val rt = list.dropWhile() { it.length > 3 } println(rt)// 輸出[Go] val map = mutableMapOf("Android" to 666) // 最後一個引數是Lambda表示式,可以將表示式寫在圓括號外面 list.associateTo(map) { it to it.length } println(map)// 輸出{Android=666, Java=4, Kotlin=6, Go=2} // 最後一個引數是Lambda表示式,可以將表示式寫在圓括號外面 val rtx = list.reduce() { acc, s -> acc + s } println(rtx)// 輸出JavaKotlinGo }
如果Lambda表示式是函式呼叫的唯一引數,呼叫方法時的圓括號可以省略。
val rt = list.dropWhile { it.length > 3 } val rtx = list.reduce { acc, s -> acc + s }
通常建議將函式型別的形參放在引數列表的最後,方便傳入Lambda表示式作為引數。
-
個數可變的引數和Lambda引數
雖然Kotlin允許將可變引數定義在形參列表的任意位置,但如果不將可變引數定義在最後,那麼就只能用命名引數的形式給可變引數之後的其他形參傳值。
但上面又建議將函式型別的引數放在形參列表的最後。如果一個函式既包含個數可變的形參,也包含函式型別的形參,這就產生了衝突。Kotlin約定:如果呼叫函式時最後一個引數是Lambda表示式,則可將Lambda表示式放在圓括號外面,這樣就無需使用命名引數了。
因此答案是:將函式型別的形參放在最後 。
fun <T> test(vararg names: String, transform: (String) -> T): List<T> { val mutableList: MutableList<T> = mutableListOf() for (name in names) { mutableList.add(transform(name)) } return mutableList } fun main(args: Array<String>) { val list1 = test("Java", "Kotlin", "Go") { it.length } println(list1)// 輸出[4, 6, 2] val list2 = test("Java", "Kotlin", "Go") { "$it${it.length}個字" } println(list2)// [Java4個字, Kotlin6個字, Go2個字] }
8. 匿名函式
Lambda表示式雖然簡介、方便但是不能指定返回值型別。大部分時候,Kotlin可以推斷出Lambda表示式的返回值型別。但在一些特殊的場景下無法推斷,就需要顯式指定返回值型別,而匿名函式即可代替Lambda表示式。
-
匿名函式的用法
匿名函式與普通函式類似,只要將普通函式去掉函式名就成了匿名函式。
fun main(args: Array<String>) { // Lambda表示式定義變數 val test1 = { x: Int, y: Int -> x + y } // 匿名函式定義變數 val test2 = fun(x: Int, y: Int): Int { return x + y } println(test1(2, 4))// 輸出6 println(test2(2, 4))// 輸出6 }
與普通函式不同的是,如果可以推斷出匿名函式的形參型別,那麼匿名函式允許省略形參型別。
fun main(args: Array<String>) { val filteredList = listOf(3, 5, 20, 100, -25).filter( // filter()方法需要傳入一個(Int) -> Boolean型別的引數 // 當傳入匿名函式時,可以推斷出引數型別必須是(Int) -> Boolean,可以省略形參型別 fun(el): Boolean { return Math.abs(el) > 20 } ) println(filteredList)// 輸出[100, -25] // filter()傳入Lambda表示式 val filteredList2 = listOf(3, 5, 20, 100, -25).filter { Math.abs(it) > 20 } println(filteredList2)// 輸出[100, -25] }
匿名函式的返回值型別的宣告規則與普通函式相同。如果使用單表示式作為函式體,則無須指定返回值型別,系統可自動推斷。
fun main(args: Array<String>) { // 單表示式作為函式體,省略返回值型別 val test = fun(x: Int, y: Int) = x + y val filteredList = listOf(3, 5, 20, 100, -25).filter( // 單表示式作為函式體,省略返回值型別 fun(el) = Math.abs(el) > 20 ) println(test(2, 4))// 輸出6 println(filteredList)// 輸出[100, -25] }
-
匿名函式和Lambda表示式的return
匿名函式的本質依然是函式,因此匿名函式中的
return
用於返回函式本身;而lambda表示式的return
用於返回它所在的函式。fun main(args: Array<String>) { val list = listOf(3, 5, 30, -25, 14) list.forEach(fun(n) { println(n)// 全部輸出 return }) list.forEach { println(it)// 只輸出3 return } }
如果一定要在Lambda中使用return,返回該函式本身,可以使用限定返回 的語法。
list.forEach { println(it)// 只輸出3 // 使用限定返回,此時return只是返回給forEach方法的Lambda表示式 return@forEach }
9. 捕獲上下文中的變數和常量
Lambda表示式或匿名函式(以及區域性函式、物件表示式)可以訪問或修改其所在上下文(俗稱“閉包”)中的變數和常量,這個過程被稱為捕獲 。即使定義這些變數和常量的作用域已經不存在,Lambda表示式或匿名函式也依然可以訪問或修改它們。
例如下面先定義一個函式,然後在該函式內定義區域性函式,此時區域性函式就可以訪問或修改其所在上下文(函式)中的變數。
// 定義一個函式,返回值型別是 () -> List<String> fun makeList(ele: String): () -> List<String> { // 建立一個不包含任何元素的List val list: MutableList<String> = mutableListOf() // 區域性函式,沒有定義任何變數,卻可以訪問list和ele,因為捕獲了其所在函式的變數 fun addElement(): List<String> { list.add(ele) return list } return ::addElement } fun main(args: Array<String>) { println("-----add1 返回的List-----") val add1 = makeList("Java") println(add1()) println(add1()) println("-----add2 返回的List-----") val add2 = makeList("Kotlin") println(add2()) println(add2()) }
執行結果
-----add1 返回的List----- [Java] [Java, Java] -----add2 返回的List----- [Kotlin] [Kotlin, Kotlin]
10. 行內函數
先簡單介紹一下高階函式(為函式傳入函式或Lambda表示式作為函式)的呼叫過程。呼叫Lambda表示式或函式的過程是:程式要將執行順序轉移到被呼叫表示式或函式所在的記憶體地址,當被呼叫表示式或函式執行完後,再返回到原函式執行的地方。
在上面這個轉移過程中,系統要處理如下事情。
- 為被呼叫的表示式或函式建立一個物件。
- 為被呼叫的表示式或函式所捕捉的變數建立一個副本。
- 在跳轉到被呼叫的表示式或函式所在的地址之前,要先保護現場並記錄執行地址;從被呼叫的表示式或函式地址返回時,要先恢復現場,並按原來儲存的地址繼續執行。也就是通常說的壓棧和彈棧。
不難看出,函式呼叫會產生一定的時間和空間開銷,如果被呼叫的表示式或函式的程式碼量本身不大,而且經常被呼叫,那麼這個時間和空間開銷的損耗就很不划算。
為了避免產生函式呼叫的過程,可以考慮直接把被呼叫的表示式或函式的程式碼“嵌入”原來的執行流中——簡單來說,就是編譯器負責“複製、貼上”:複製被呼叫的表示式或函式的程式碼,然後貼上到原來的執行程式碼中。為了讓編譯器幫我們幹這個複製、貼上的話,可通過行內函數來實現。
-
行內函數的使用
只要使用
inline
關鍵字修飾帶函式形參的函式即可。下面示範來行內函數和非行內函數的區別。inline fun map(data: Array<Int>, fn: (Int) -> Int): Array<Int> { val result = Array(data.size) { 0 } for (i in data.indices) { result[i] = fn(data[i]) } return result } fun main(args: Array<String>) { val arr = arrayOf(20, 4, 40, 100, 30) val mappedResult = map(arr) { it + 3 } println(mappedResult.contentToString()) }
使用
inline
行內函數,編譯器實際上會將Lambda表示式的程式碼複製、貼上到map()
函式中。也就是說,程式呼叫的map
函式編譯後實際上變成了如下形式:fun map(data: Array<Int>): Array<Int> { val result = Array(data.size) { 0 } for (i in data.indices) { result[i] = data[i] + 3 } return result }
需要注意的是,行內函數並不總是能帶來好處,因為行內函數的本質是將被呼叫的Lambada表示式或函式的程式碼複製、貼上到原執行函式中。當Lambada表示式或函式包含大量的執行程式碼,不應使用行內函數 ;如果Lambada表示式或函式只包含非常簡單的執行程式碼(尤其是單表示式),那麼就應該使用行內函數 。
-
部分禁止內聯
使用
inline
修飾後,所有傳入函式的Lambda表示式或函式都會被內聯化,如果希望該函式中的某個或某幾個函式型別的形參不被內聯化,可以使用noinline
修飾。inline fun test(fn1: (Int) -> Int, noinline fn2: (String) -> String) { println(fn1(8)) println(fn2("Kotlin")) } fun main(args: Array<String>) { test({ it * it }, { it }) }
上面程式雖然使用
inline
修飾了test()
函式,但是fn2
形參使用了noinline
修飾,它不會被內聯化。 -
非區域性返回
前面提到在Lambda表示式中使用
return
不是用於返回該表示式,而是返回該表示式所在的函式。但要記住:預設情況下,在Lambda表示式中並不允許直接使用return
。這是因為如果是非內聯的Lambda表示式會額外生成一個函式物件,因此這種表示式中的return
不可能用於返回它所在的函式。由於內聯的Lambda表示式會被直接複製、貼上到呼叫它的函式中,故此在該Lambda表示式中可使用
return
,該return
就像直接寫在Lambda表示式的呼叫函式中一樣。因此,該內聯的Lambda表示式中的return
可用於返回它所在的函式,這種返回被稱作非區域性返回 。inline fun each(data: Array<Int>, fn: (Int) -> Unit) { for (el in data) { fn(el) } } fun main(args: Array<String>) { val arr = arrayOf(20, 4, 40, 100, 30) each(arr) { if (it == 4) { return@each// 返回each函式 } if (it == 100) { //return// 如果each沒有inline修飾,此處編譯異常 return // 如果each有inline修飾,return返回main函式 } println(it) } }
如果刪除上面
each()
函式的inline
修飾符,那麼下面的Lambda表示式中的return
將會提示編譯錯誤:'return' is not allowed here
,這意味著在非內聯的Lambda表示式中不能使用return
。另外,有些行內函數不是從函式體中呼叫Lambda表示式的,而是從其他的執行上下文(如區域性物件或區域性函式)中來獲取Lambda表示式的。在這種情況下,非區域性返回的控制流也不與許出現在Lambda表示式中。此時應該使用
crossinline
來修飾該引數。inline fun f(crossinline body: () -> Unit) { val f1 = object : Runnable { override fun run() { body() } } // Lambda簡寫,和上面f1一樣 val f2 = Runnable { body() } }
重點
- 函式、呼叫函式的語法
- 函式形參的外部形參名、形參預設值、常量形參和可變引數、In-Out形參等高階特性
- 函式型別可以被當作陣列型別使用,即可用於宣告變數,也可作為形參或者返回值
- Lambda表示式