一、內(nèi)聯(lián)函數(shù)原理
使用高階函數(shù)為開發(fā)帶來了便利,但同時(shí)也產(chǎn)生了一些性能上的損失,官方是這樣描述這個(gè)問題:
使用高階函數(shù)會(huì)帶來一些運(yùn)行時(shí)的效率損失:每一個(gè)函數(shù)都是一個(gè)對(duì)象,并且會(huì)捕獲一個(gè)閉包。 即那些在函數(shù)體內(nèi)會(huì)訪問到的變量。 內(nèi)存分配(對(duì)于函數(shù)對(duì)象和類)和虛擬調(diào)用會(huì)引入運(yùn)行時(shí)間開銷,但是通過內(nèi)聯(lián)化 Lambda 表達(dá)式可以消除這類的開銷。
為了解決這個(gè)問題,可以使用內(nèi)聯(lián)函數(shù),用inline修飾的函數(shù)就是內(nèi)聯(lián)函數(shù),inline修飾符影響函數(shù)本身和傳給它的 Lambda 表達(dá)式,所有這些都將內(nèi)聯(lián)到調(diào)用處,即編譯器會(huì)把調(diào)用這個(gè)函數(shù)的地方用這個(gè)函數(shù)的方法體進(jìn)行替換,而不是創(chuàng)建一個(gè)函數(shù)對(duì)象并生成一個(gè)調(diào)用。
接下來用代碼驗(yàn)證這個(gè)說法,先定義一個(gè)普通的高階函數(shù),然后調(diào)用兩次:
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
這樣其實(shí)是看不出什么問題的,Kotlin 文件編譯后會(huì)生成對(duì)應(yīng)的 class 文件,所以我們將 class 文件反編譯成 Java 文件后再看。如果使用Android Studio或者IntelliJ IDEA,可以按照如下方式查看 Kotlin 文件對(duì)應(yīng)反編譯后的 Java 文件:
- 打開目標(biāo) Kotlin 文件
- 查看 Kotlin 文件字節(jié)碼:Tools –> Kotlin –> Show Kotlin ByteCode
- 在 kotlin 文件字節(jié)碼頁面中點(diǎn)擊左上角的 decompile 按鈕,就會(huì)生成對(duì)應(yīng)的 Java 文件
我們來看上邊代碼對(duì)應(yīng)的 Java 代碼:

雖然不是正常的 Java 代碼,但不妨礙我們分析流程,可以看出,編譯器創(chuàng)建了兩個(gè) Lambda 的實(shí)例,并進(jìn)行了兩次
calculate函數(shù)調(diào)用。
那如果將calculate聲明為內(nèi)聯(lián)函數(shù)呢:
inline fun calculate(a: Int, b: Int, cal: (Int, Int) -> String) {
println(cal(a, b))
}
我們?cè)倏醋罱K的 Java 文件:

即編譯器會(huì)把調(diào)用這個(gè)函數(shù)的地方用這個(gè)函數(shù)的方法體進(jìn)行替換,這樣驗(yàn)證了之前的說法。
需要注意的是, 內(nèi)聯(lián)函數(shù)提高代碼性能的同時(shí)也會(huì)導(dǎo)致代碼量的增加,所以應(yīng)避免內(nèi)聯(lián)函數(shù)過大。
二、禁用內(nèi)聯(lián)(noinline)
如果一個(gè)內(nèi)聯(lián)函數(shù)可以接收多個(gè) Lambda 表達(dá)式作為參數(shù),默認(rèn)這些 Lambda 表達(dá)式都會(huì)被內(nèi)內(nèi)聯(lián)到調(diào)用處,如果需要某個(gè) Lambda 表達(dá)式不被內(nèi)聯(lián),可以使用noinline修飾對(duì)應(yīng)的函數(shù)參數(shù):
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("開始計(jì)算") }) { a, b ->
"$a * $b = ${a * b}"
}
}
// 輸出
開始計(jì)算
3 * 7 = 21
title對(duì)應(yīng)的 Lambda 確實(shí)沒有被內(nèi)聯(lián),看圖:

一個(gè)內(nèi)聯(lián)函數(shù)沒有可內(nèi)聯(lián)的函數(shù)參數(shù)并且沒有具體化的類型參數(shù),編譯器會(huì)有警告,因?yàn)檫@樣并不能帶來什么好處,如果你不愿去掉內(nèi)聯(lián)修飾,可以使用@Suppress("NOTHING_TO_INLINE") 注解關(guān)閉這個(gè)警告。
三、非局部返回
我們知道默認(rèn)情況下,在高階函數(shù)中,要顯式的退出(返回)一個(gè) Lambda 表達(dá)式,需要使用 return@標(biāo)簽的語法,不能使用裸return,但這樣也不能使高階函數(shù)和包含高階函數(shù)的函數(shù)退出。例如:
fun message(block: () -> Unit) {
block()
println("-----")
}
fun test() {
message {
println("Hello")
return@message
}
println("World")
}
fun main(args: Array<String>) {
test()
}
// 輸出
Hello
-----
World
但如果把 Lambda 表達(dá)式作為參數(shù)傳遞給一個(gè)內(nèi)聯(lián)函數(shù),就可以在 Lambda 表達(dá)式中正常的使用return語句了,并且會(huì)使該內(nèi)聯(lián)函數(shù)和包含該內(nèi)聯(lián)函數(shù)的函數(shù)退出(返回),這種操作就是非局部返回。例如:
inline fun message(block: () -> Unit) {
block()
println("-----")
}
fun test() {
message {
println("Hello")
return
}
println("World")
}
fun main(args: Array<String>) {
test()
}
// 輸出
Hello
注意,由于非局部返回的原因,這里只輸出了Hello。
在使用了非局部返回后,Lambda 表達(dá)式中return的返回值受調(diào)用該內(nèi)聯(lián)函數(shù)的函數(shù)的返回值類型影響。例如:
fun test(): Boolean {
message {
println("Hello")
return false
}
println("World")
return true
}
四、禁用非局部返回(crossinline)
從前邊已經(jīng)知道,通過內(nèi)聯(lián)函數(shù)可以使 Lambda表達(dá)式實(shí)現(xiàn)非局部返回,但是,如果一個(gè)內(nèi)聯(lián)函數(shù)的函數(shù)類型參數(shù)被crossinline修飾,則對(duì)應(yīng)傳入的 Lambda表達(dá)式將不能非局部返回了,只能局部返回了。還是用之前的例子修改:
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可以禁用掉非局部返回,但有什么意義呢?這其實(shí)是有實(shí)際的場景需求的,看個(gè)例子:
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)
}
首先定義一個(gè)Calculator計(jì)算接口,然后在內(nèi)聯(lián)函數(shù)test中創(chuàng)建Calculator的一個(gè)對(duì)象表達(dá)式,重寫calculate方法時(shí),我們讓calculate的函數(shù)體是test函數(shù)的block參數(shù),當(dāng)block是 Lambda表達(dá)式時(shí),由于非局部返回的原因,導(dǎo)致calculate函數(shù)的返回值不是預(yù)期的,進(jìn)而發(fā)生異常,為了避免這種情況的發(fā)生,所以就有必要使用crossinline來禁用非局部返回,來保證calculate的返回值類型是安全的。
上邊的代碼會(huì)有這樣一個(gè)錯(cuò)誤提示:
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)
}
五、具體化的類型參數(shù)(reified)
對(duì)于一個(gè)泛型函數(shù),如果需要訪問泛型參數(shù)的類型,但由于泛型類型被擦除的原因,可能無法直接訪問,但通過反射還是可以做到的,例如:
fun <T> test(param: Any, clazz: Class<T>) {
if (clazz.isInstance(param)) {
println("參數(shù)類型匹配")
} else {
println("參數(shù)類型不匹配")
}
}
fun main(args: Array<String>) {
test("Hello World", String::class.java)
test(666, String::class.java)
}
// 輸出
參數(shù)類型匹配
參數(shù)類型不匹配
功能雖然實(shí)現(xiàn)了,但是不夠優(yōu)雅,Kotlin 中有更好的辦法來實(shí)現(xiàn)這樣的功能。
在內(nèi)聯(lián)函數(shù)中支持具體化的參數(shù)類型,即用reified來修飾需要具體化的參數(shù)類型,這樣我們用reified來修飾泛型的參數(shù)類型,以達(dá)到我們的目的:
inline fun <reified T> test(param: Any) {
if (param is T) {
println("參數(shù)類型匹配")
} else {
println("參數(shù)類型不匹配")
}
}
調(diào)用的過程也變得簡單了:
fun main(args: Array<String>) {
test<String>("Hello World")
test<String>(666)
}