一办铡、內(nèi)聯(lián)函數(shù)原理
使用高階函數(shù)為開發(fā)帶來了便利脐雪,但同時也產(chǎn)生了一些性能上的損失付翁,官方是這樣描述這個問題:
使用高階函數(shù)會帶來一些運(yùn)行時的效率損失:每一個函數(shù)都是一個對象尖阔,并且會捕獲一個閉包贮缅。 即那些在函數(shù)體內(nèi)會訪問到的變量。 內(nèi)存分配(對于函數(shù)對象和類)和虛擬調(diào)用會引入運(yùn)行時間開銷诺祸,但是通過內(nèi)聯(lián)化 Lambda 表達(dá)式可以消除這類的開銷携悯。
為了解決這個問題,可以使用內(nèi)聯(lián)函數(shù)筷笨,用inline
修飾的函數(shù)就是內(nèi)聯(lián)函數(shù)憔鬼,inline
修飾符影響函數(shù)本身和傳給它的 Lambda 表達(dá)式,所有這些都將內(nèi)聯(lián)到調(diào)用處胃夏,即編譯器會把調(diào)用這個函數(shù)的地方用這個函數(shù)的方法體進(jìn)行替換轴或,而不是創(chuàng)建一個函數(shù)對象并生成一個調(diào)用。
接下來用代碼驗證這個說法仰禀,先定義一個普通的高階函數(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
這樣其實是看不出什么問題的,Kotlin 文件編譯后會生成對應(yīng)的 class 文件答恶,所以我們將 class 文件反編譯成 Java 文件后再看饺蚊。如果使用Android Studio或者IntelliJ IDEA,可以按照如下方式查看 Kotlin 文件對應(yīng)反編譯后的 Java 文件:
- 打開目標(biāo) Kotlin 文件
- 查看 Kotlin 文件字節(jié)碼:Tools –> Kotlin –> Show Kotlin ByteCode
- 在 kotlin 文件字節(jié)碼頁面中點擊左上角的 decompile 按鈕悬嗓,就會生成對應(yīng)的 Java 文件
我們來看上邊代碼對應(yīng)的 Java 代碼:
雖然不是正常的 Java 代碼污呼,但不妨礙我們分析流程,可以看出包竹,編譯器創(chuàng)建了兩個 Lambda 的實例燕酷,并進(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))
}
我們再看最終的 Java 文件:
即編譯器會把調(diào)用這個函數(shù)的地方用這個函數(shù)的方法體進(jìn)行替換,這樣驗證了之前的說法苗缩。
需要注意的是饵蒂, 內(nèi)聯(lián)函數(shù)提高代碼性能的同時也會導(dǎo)致代碼量的增加,所以應(yīng)避免內(nèi)聯(lián)函數(shù)過大酱讶。
二退盯、禁用內(nèi)聯(lián)(noinline)
如果一個內(nèi)聯(lián)函數(shù)可以接收多個 Lambda 表達(dá)式作為參數(shù),默認(rèn)這些 Lambda 表達(dá)式都會被內(nèi)內(nèi)聯(lián)到調(diào)用處浴麻,如果需要某個 Lambda 表達(dá)式不被內(nèi)聯(lián)得问,可以使用noinline
修飾對應(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("開始計算") }) { a, b ->
"$a * $b = ${a * b}"
}
}
// 輸出
開始計算
3 * 7 = 21
title
對應(yīng)的 Lambda 確實沒有被內(nèi)聯(lián),看圖:
一個內(nèi)聯(lián)函數(shù)沒有可內(nèi)聯(lián)的函數(shù)參數(shù)并且沒有具體化的類型參數(shù)软免,編譯器會有警告,因為這樣并不能帶來什么好處焚挠,如果你不愿去掉內(nèi)聯(lián)修飾膏萧,可以使用@Suppress("NOTHING_TO_INLINE")
注解關(guān)閉這個警告。
三蝌衔、非局部返回
我們知道默認(rèn)情況下榛泛,在高階函數(shù)中,要顯式的退出(返回)一個 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ù)傳遞給一個內(nèi)聯(lián)函數(shù)沛简,就可以在 Lambda 表達(dá)式中正常的使用return
語句了,并且會使該內(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á)式實現(xiàn)非局部返回饲窿,但是煌寇,如果一個內(nèi)聯(lián)函數(shù)的函數(shù)類型參數(shù)被crossinline
修飾,則對應(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
可以禁用掉非局部返回,但有什么意義呢?這其實是有實際的場景需求的淌哟,看個例子:
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
計算接口迹卢,然后在內(nèi)聯(lián)函數(shù)test
中創(chuàng)建Calculator
的一個對象表達(dá)式,重寫calculate
方法時徒仓,我們讓calculate
的函數(shù)體是test
函數(shù)的block
參數(shù)腐碱,當(dāng)block
是 Lambda表達(dá)式時,由于非局部返回的原因掉弛,導(dǎo)致calculate
函數(shù)的返回值不是預(yù)期的症见,進(jìn)而發(fā)生異常,為了避免這種情況的發(fā)生殃饿,所以就有必要使用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)
}
五乎芳、具體化的類型參數(shù)(reified)
對于一個泛型函數(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ù)類型不匹配
功能雖然實現(xiàn)了肴甸,但是不夠優(yōu)雅寂殉,Kotlin 中有更好的辦法來實現(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)
}