Kotlin內聯函數

內聯函數

使用高階函數會帶來一些運行時的效率損失:每一個函數都是一個對象,並且會捕獲一個閉包。
即那些在函數體內會訪問到的變量。
內存分配(對於函數對象和類)和虛擬調用會引入運行時間開銷。

但是在許多情況下通過內聯化 lambda 表達式可以消除這類的開銷。
下述函數是這種情況的很好的例子。即 lock() 函數可以很容易地在調用處內聯。
考慮下面的情況:

lock(l) { foo() }

編譯器沒有爲參數創建一個函數對象並生成一個調用。取而代之,編譯器可以生成以下代碼:

l.lock()
try {
    foo()
}
finally {
    l.unlock()
}

這個不是我們從一開始就想要的嗎?

爲了讓編譯器這麼做,我們需要使用 inline 修飾符標記 lock() 函數:

inline fun lock<T>(lock: Lock, body: () -> T): T {
    // ……
}

inline 修飾符影響函數本身和傳給它的 lambda 表達式:所有這些都將內聯
到調用處。

內聯可能導致生成的代碼增加,但是如果我們使用得當(不內聯大函數),它將在
性能上有所提升,尤其是在循環中的「超多態(megamorphic)」調用處。

禁用內聯

如果你只想被(作爲參數)傳給一個內聯函數的 lamda 表達式中只有一些被內聯,你可以用 noinline 修飾符標記
一些函數參數:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
    // ……
}

可以內聯的 lambda 表達式只能在內聯函數內部調用或者作爲可內聯的參數傳遞,
但是 noinline 的可以以任何我們喜歡的方式操作:存儲在字段中、傳送它等等。

需要注意的是,如果一個內聯函數沒有可內聯的函數參數並且沒有
具體化的類型參數,編譯器會產生一個警告,因爲內聯這樣的函數
很可能並無益處(如果你確認需要內聯,則可以關掉該警告)。

非局部返回

在 Kotlin 中,我們可以只使用一個正常的、非限定的 return 來退出一個命名或匿名函數。
這意味着要退出一個 lambda 表達式,我們必須使用一個標籤,並且
在 lambda 表達式內部禁止使用裸 return,因爲 lambda 表達式不能使包含它的函數返回:

fun foo() {
    ordinaryFunction {
        return // 錯誤:不能使 `foo` 在此處返回
    }
}

但是如果 lambda 表達式傳給的函數是內聯的,該 return 也可以內聯,所以它是允許的:

fun foo() {
    inlineFunction {
        return // OK:該 lambda 表達式是內聯的
    }
}

這種返回(位於 lambda 表達式中,但退出包含它的函數)稱爲非局部返回。 我們習慣了
在循環中用這種結構,其內聯函數通常包含:

fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if (it == 0) return true // 從 hasZeros 返回
    }
    return false
}

請注意,一些內聯函數可能調用傳給它們的不是直接來自函數體、而是來自另一個執行
上下文的 lambda 表達式參數,例如來自局部對象或嵌套函數。在這種情況下,該 lambda 表達式中
也不允許非局部控制流。爲了標識這種情況,該 lambda 表達式參數需要
crossinline 修飾符標記:

inline fun f(crossinline body: () -> Unit) {
    val f = object: Runnable {
        override fun run() = body()
    }
    // ……
}

breakcontinue 在內聯的 lambda 表達式中還不可用,但我們也計劃支持它們

具體化的類型參數

有時候我們需要訪問一個作爲參數傳給我們的一個類型:

fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
    var p = parent
    while (p != null && !clazz.isInstance(p)) {
        p = p.parent
    }
    @Suppress("UNCHECKED_CAST")
    return p as T?
}

在這裏我們向上遍歷一棵樹並且檢查每個節點是不是特定的類型。
這都沒有問題,但是調用處不是很優雅:

treeNode.findParentOfType(MyTreeNode::class.java)

我們真正想要的只是傳一個類型給該函數,即像這樣調用它:

treeNode.findParentOfType<MyTreeNode>()

爲能夠這麼做,內聯函數支持具體化的類型參數,於是我們可以這樣寫:

inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}

我們使用 reified 修飾符來限定類型參數,現在可以在函數內部訪問它了,
幾乎就像是一個普通的類一樣。由於函數是內聯的,不需要反射,正常的操作符如 !is
as 現在都能用了。此外,我們還可以按照上面提到的方式調用它:myTree.findParentOfType<MyTreeNodeType>()

雖然在許多情況下可能不需要反射,但我們仍然可以對一個具體化的類型參數使用它:

inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
    println(membersOf<StringBuilder>().joinToString("\n"))
}

普通的函數(未標記爲內聯函數的)不能有具體化參數。
不具有運行時表示的類型(例如非具體化的類型參數或者類似於Nothing的虛構類型)
不能用作具體化的類型參數的實參。

相關底層描述,請參見規範文檔

內聯屬性(自 1.1 起)

inline 修飾符可用於沒有幕後字段的屬性的訪問器。
你可以標註獨立的屬性訪問器:

val foo: Foo
    inline get() = Foo()

var bar: Bar
    get() = ……
    inline set(v) { …… }

你也可以標註整個屬性,將它的兩個訪問器都標記爲內聯:

inline var bar: Bar
    get() = ……
    set(v) { …… }

在調用處,內聯訪問器如同內聯函數一樣內聯。