Kotlin 行內函數
一、行內函數原理
使用高階函式為開發帶來了便利,但同時也產生了一些效能上的損失,官方是這樣描述這個問題:
使用高階函式會帶來一些執行時的效率損失:每一個函式都是一個物件,並且會捕獲一個閉包。 即那些在函式體內會訪問到的變數。 記憶體分配(對於函式物件和類)和虛擬呼叫會引入執行時間開銷,但是通過內聯化 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/">Android Studio 或者 IntelliJ IDEA ,可以按照如下方式檢視 Kotlin 檔案對應反編譯後的 Java 檔案:
- 開啟目標 Kotlin 檔案
- 檢視 Kotlin 檔案位元組碼:Tools –> Kotlin –> Show Kotlin ByteCode
- 在 kotlin 檔案位元組碼頁面中點選左上角的 decompile 按鈕,就會生成對應的 Java 檔案
我們來看上邊程式碼對應的 Java 程式碼:

1
calculate
函式呼叫。
那如果將 calculate
宣告為行內函數呢:
inline fun calculate(a: Int, b: Int, cal: (Int, Int) -> String) { println(cal(a, b)) }
我們再看最終的 Java 檔案:

2
即編譯器會把呼叫這個函式的地方用這個函式的方法體進行替換,這樣驗證了之前的說法。
需要注意的是, 行內函數提高程式碼效能的同時也會導致程式碼量的增加,所以應避免行內函數過大。
二、禁用內聯(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 確實沒有被內聯,看圖:

3
一個行內函數沒有可內聯的函式引數並且沒有具體化的型別引數,編譯器會有警告,因為這樣並不能帶來什麼好處,如果你不願去掉內聯修飾,可以使用 @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) }