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或者IntelliJ IDEA,可以按照如下方式查看 Kotlin 文件對應反編譯後的 Java 文件:

  1. 打開目標 Kotlin 文件
  2. 查看 Kotlin 文件字節碼:Tools –> Kotlin –> Show Kotlin ByteCode
  3. 在 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)
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章