Kotlin 內(nèi)聯(lián)函數(shù)

一、內(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 文件:

  1. 打開目標(biāo) Kotlin 文件
  2. 查看 Kotlin 文件字節(jié)碼:Tools –> Kotlin –> Show Kotlin ByteCode
  3. 在 kotlin 文件字節(jié)碼頁面中點(diǎn)擊左上角的 decompile 按鈕,就會(huì)生成對(duì)應(yīng)的 Java 文件

我們來看上邊代碼對(duì)應(yīng)的 Java 代碼:

1

雖然不是正常的 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 文件:


2

即編譯器會(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),看圖:

3

一個(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)
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容