一、內聯函數原理
使用高階函數爲開發帶來了便利,但同時也產生了一些性能上的損失,官方是這樣描述這個問題:
使用高階函數會帶來一些運行時的效率損失:每一個函數都是一個對象,並且會捕獲一個閉包。 即那些在函數體內會訪問到的變量。 內存分配(對於函數對象和類)和虛擬調用會引入運行時間開銷,但是通過內聯化 Lambda 表達式可以消除這類的開銷。
爲了解決這個問題,可以使用內聯函數,用inline
修飾的函數就是內聯函數,inline
修飾符影響函數本身和傳給它的 Lambda 表達式,所有這些都將內聯到調用處,即編譯器會把調用這個函數的地方用這個函數的方法體進行替換,而不是創建一個函數對象並生成一個調用。
接下來用代碼驗證這個說法,先定義一個普通的高階函數,然後調用兩次:
fun calculate(a: Int, b: Int, cal: (Int, Int) -> String) {
println(cal(a, b))
}
fun main(args: Array<String>) {
calculate(3, 7) { a, b ->
"$a + $b = ${a + b}"
}
calculate(3, 7) { a, b ->
"$a * $b = ${a * b}"
}
}
// 輸出
3 + 7 = 10
3 * 7 = 21
這樣其實是看不出什麼問題的,Kotlin 文件編譯後會生成對應的 class 文件,所以我們將 class 文件反編譯成 Java 文件後再看。如果使用Android Studio或者IntelliJ IDEA,可以按照如下方式查看 Kotlin 文件對應反編譯後的 Java 文件:
- 打開目標 Kotlin 文件
- 查看 Kotlin 文件字節碼:Tools –> Kotlin –> Show Kotlin ByteCode
- 在 kotlin 文件字節碼頁面中點擊左上角的 decompile 按鈕,就會生成對應的 Java 文件
我們來看上邊代碼對應的 Java 代碼:
雖然不是正常的 Java 代碼,但不妨礙我們分析流程,可以看出,編譯器創建了兩個 Lambda 的實例,並進行了兩次
calculate
函數調用。
那如果將calculate
聲明爲內聯函數呢:
inline fun calculate(a: Int, b: Int, cal: (Int, Int) -> String) {
println(cal(a, b))
}
我們再看最終的 Java 文件:
即編譯器會把調用這個函數的地方用這個函數的方法體進行替換,這樣驗證了之前的說法。
需要注意的是, 內聯函數提高代碼性能的同時也會導致代碼量的增加,所以應避免內聯函數過大。
二、禁用內聯(noinline)
如果一個內聯函數可以接收多個 Lambda 表達式作爲參數,默認這些 Lambda 表達式都會被內內聯到調用處,如果需要某個 Lambda 表達式不被內聯,可以使用noinline
修飾對應的函數參數:
inline fun calculate(a: Int, b: Int, noinline title: () -> Unit, cal: (Int, Int) -> String) {
title()
println(cal(a, b))
}
fun main(args: Array<String>) {
calculate(3, 7, { println("開始計算") }) { a, b ->
"$a * $b = ${a * b}"
}
}
// 輸出
開始計算
3 * 7 = 21
title
對應的 Lambda 確實沒有被內聯,看圖:
一個內聯函數沒有可內聯的函數參數並且沒有具體化的類型參數,編譯器會有警告,因爲這樣並不能帶來什麼好處,如果你不願去掉內聯修飾,可以使用@Suppress("NOTHING_TO_INLINE")
註解關閉這個警告。
三、非局部返回
我們知道默認情況下,在高階函數中,要顯式的退出(返回)一個 Lambda 表達式,需要使用 return@標籤
的語法,不能使用裸return
,但這樣也不能使高階函數和包含高階函數的函數退出。例如:
fun message(block: () -> Unit) {
block()
println("-----")
}
fun test() {
message {
println("Hello")
return@message
}
println("World")
}
fun main(args: Array<String>) {
test()
}
// 輸出
Hello
-----
World
但如果把 Lambda 表達式作爲參數傳遞給一個內聯函數,就可以在 Lambda 表達式中正常的使用return
語句了,並且會使該內聯函數和包含該內聯函數的函數退出(返回),這種操作就是非局部返回
。例如:
inline fun message(block: () -> Unit) {
block()
println("-----")
}
fun test() {
message {
println("Hello")
return
}
println("World")
}
fun main(args: Array<String>) {
test()
}
// 輸出
Hello
注意,由於非局部返回的原因,這裏只輸出了Hello
。
在使用了非局部返回後,Lambda 表達式中return
的返回值受調用該內聯函數的函數的返回值類型影響。例如:
fun test(): Boolean {
message {
println("Hello")
return false
}
println("World")
return true
}
四、禁用非局部返回(crossinline)
從前邊已經知道,通過內聯函數可以使 Lambda表達式實現非局部返回,但是,如果一個內聯函數的函數類型參數被crossinline
修飾,則對應傳入的 Lambda表達式將不能非局部返回了,只能局部返回了。還是用之前的例子修改:
inline fun message(crossinline block: () -> Unit) {
block()
println("-----")
}
fun test() {
message {
println("Hello")
return@message
}
println("World")
}
fun main(args: Array<String>) {
test()
}
// 輸出
Hello
-----
World
通過crossinline
可以禁用掉非局部返回,但有什麼意義呢?這其實是有實際的場景需求的,看個例子:
interface Calculator {
fun calculate(a: Int, b: Int): Int
}
inline fun test(block: (Int, Int) -> Int) {
val c = object : Calculator {
override fun calculate(a: Int, b: Int): Int = block(a, b)
}
c.calculate(3, 7)
}
首先定義一個Calculator
計算接口,然後在內聯函數test
中創建Calculator
的一個對象表達式,重寫calculate
方法時,我們讓calculate
的函數體是test
函數的block
參數,當block
是 Lambda表達式時,由於非局部返回的原因,導致calculate
函數的返回值不是預期的,進而發生異常,爲了避免這種情況的發生,所以就有必要使用crossinline
來禁用非局部返回,來保證calculate
的返回值類型是安全的。
上邊的代碼會有這樣一個錯誤提示:
Can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block'
使用crossinline
後就正常了:
inline fun test(crossinline block: (Int, Int) -> Int) {
val c = object : Calculator {
override fun calculate(a: Int, b: Int): Int = block(a, b)
}
c.calculate(3, 7)
}
五、具體化的類型參數(reified)
對於一個泛型函數,如果需要訪問泛型參數的類型,但由於泛型類型被擦除的原因,可能無法直接訪問,但通過反射還是可以做到的,例如:
fun <T> test(param: Any, clazz: Class<T>) {
if (clazz.isInstance(param)) {
println("參數類型匹配")
} else {
println("參數類型不匹配")
}
}
fun main(args: Array<String>) {
test("Hello World", String::class.java)
test(666, String::class.java)
}
// 輸出
參數類型匹配
參數類型不匹配
功能雖然實現了,但是不夠優雅,Kotlin 中有更好的辦法來實現這樣的功能。
在內聯函數中支持具體化的參數類型,即用reified
來修飾需要具體化的參數類型,這樣我們用reified
來修飾泛型的參數類型,以達到我們的目的:
inline fun <reified T> test(param: Any) {
if (param is T) {
println("參數類型匹配")
} else {
println("參數類型不匹配")
}
}
調用的過程也變得簡單了:
fun main(args: Array<String>) {
test<String>("Hello World")
test<String>(666)
}