天天看點

Kotlin學習(十八)—— 内聯函數

為什麼要有内聯函數

使用高階函數會帶來⼀些運⾏時的效率損失:每⼀個函數都是⼀個對象,并且會捕獲⼀個閉包。即那些在函數體内會通路到的變量。記憶體配置設定(對于函數對象和類)和虛拟調用會引⼊運作時間開銷。

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

考慮下⾯的情況:

fun <T> lock(lock: Lock, body :() -> T):T {
    lock.lock()
    try {
        return body()
    } finally {
        lock.unlock()
    }
}
fun foo(){
    println("函數被調用了")
}
//接下來我們調用lock()函數
fun main(args: Array<String>) {
    lock(ReentrantLock()) {
        foo()
    }
}
           

上面的代碼傳入了一個lambda表達式,編譯期會把這個表達式看做一個對象處理。而不是像下面的這樣把代碼像如下的方式生成:

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

為了讓編譯器生成上面代碼,我們就要使用inline标記lock()函數

inline fun <T> lock(lock: Lock, body :() -> T):T {
    lock.lock()
    try {
        return body()
    } finally {
        lock.unlock()
    }
}
           

上面的lock()函數被iniline修飾了,調用lock()時,lock函數中的代碼,和傳入它的表達式,都會代碼原樣的被編譯器重新複制一遍到調用lock()的地方。

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

内聯可能導緻生成的代碼增加,但是如果我們使用得當(不内聯大函數),它将在性能上有所提升,尤其是在循環中的“超多态(megamorphic)”調用處。

(這裡小編并不知道好多态是什麼東西)

禁用内聯

如果你隻想被(作為參數)傳給⼀個内聯函數的 lamda 表達式中隻有⼀些被内聯,你可以用 noinline 修飾符标記⼀些函數參數,如下:

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

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

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

非局部傳回

在 Kotlin 中,我們可以隻使用一個正常的、非限定的 return 來退出⼀個命名或匿名函數。這意味着要退出⼀個 lambda 表達式,我們必須使用⼀個标簽,并且在 lambda 表達式内部禁止使用裸 return ,因為 lambda 表達式不能使包含它的函數傳回。

這是因為lambda 表達式中的 return 将從包含它的函數傳回,⽽匿名函數中的 return 将從匿名函數⾃⾝傳回。如果lambda使用裸傳回,包含他的函數也會傳回(一般情況下,編譯期禁止lambda裸傳回)

fun say(sayHello : () -> Unit){
    println(sayHello())
}
fun main(args: Array<String>) {
    say {
        //return //這裡會報錯
        return@say //這裡使用标簽就不會錯
    }
}
           

但是如果 lambda 表達式傳給的函數是内聯的,該 return 也可以内聯,是以它(裸return)是允許的

實際上是因為

inline fun say(sayHello : () -> String){
    println(sayHello())
    println("lambda表達式傳回") //這句話能執行
}

fun main(args: Array<String>) {
    say {
        return@say "hello" //這裡使用标簽就不會錯,因為這裡是從lambda表達式傳回
        return //這裡也不會報錯,這裡是從say()傳回,但是這不會被執行,因為lambda表達式已經傳回了
    }
}
           

但是需要注意的是,上面的lambda表達式還是不能使用return “hello”老代替[email protected] “hello”,因為lambda表達式使用return還是一樣的從包裹它的函數傳回

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

fun hasZero(array: Array<Int>):Boolean{
    for (i in array){
        if (i == ) return true
    }
    return false
}
           

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

//雖然這個函數式内聯的,但是函數參數被crossinline标記,是以傳入的表達式不允許非局部傳回
inline fun f(crossinline body: () -> Unit) {  
    val f = object: Runnable {
        override fun run() = body()
    }
    f.run()
}
fun main(args: Array<String>) {
    f { 
        return  //這裡編譯期會報錯,因為不允許非局部傳回
    }
}
           

下面這種情況也是一樣的,不能非局部傳回:

inline fun compare(crossinline body:(value:Int,other :Int) -> Boolean){
    var  a= 9
    val c = object : Comparable<Int>{
         val value = a
         override fun compareTo(other: Int): Int {
             if (body(value,other)){
                 return 1
             }else{
                 return -1
             }
        }
    }
    println(c.compareTo())
}
fun main(args: Array<String>) {
    compare(){
        x,y ->if (x>y)true else false
        //return在這裡同樣不允許使用
    }
}
           

break 和 continue 在内聯的 lambda 表達式中在Kotlin中還不可⽤,但是也計劃⽀持它們(小編希望kotlin能在語言榜排進前十,kotlin有日趨完善,因為kotlin真心是一個好的語言)

具體化的類型參數

有時候我們需要通路⼀個作為參數傳給我們的⼀個類型:

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>()
           

為能夠這麼做,内聯函數⽀持具體化的類型參數,于是我們可以這樣寫

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 的虛構類型)不能用作具體化的類型參數的實參

内聯屬性

inline 修飾符可⽤于沒有幕後字段的屬性的通路器。你可以标注獨⽴的屬性通路器:

class A {
    val s : String
        inline get() = String("hello".toByteArray())
    inline var s1 : String
        get() = String("lala".toCharArray())
        set(value) {
            println("$value")
        }

}

fun main(args: Array<String>) {
    var a = A()
    a.s1="lal"
    println(a.s1)
}
           

公有 API 内聯函數的限制

當⼀個内聯函數是 public 或 protected 而不是 private 或 internal 聲明的⼀部分時,就會認為它是⼀個子產品級的公有 API。可以在其他子產品中調用它,并且也可以在調用處内聯這樣的調用。

這帶來了⼀些由子產品做這樣變更時導緻的⼆進制相容的⻛險⸺聲明⼀個内聯函數但調用它的子產品在它修改後并沒有重新編譯。(A 調用了内聯函數B,然後B修改了,A的調用處是不會重新編譯的)

為了消除這種由非公有 API 變更引⼊的不相容的風險,公有 API 内聯函數體内不允許使用非公有聲明,即,不允許使用private 與 internal 聲明以及其部件。

private fun hello(){
    println("hello")
}

inline fun foo(){
    //下面的函數調用會報錯
    hello()  //foo()是共有的内聯API,是以為了避免二進制風險,函數體裡面不能調用非公有的API(hello()函數式私有的)
}
           

上面的代碼編譯期會報錯:Public-API inline function cannot access non-public-API ‘private fun hello(): Unit defined in B_functionAndLambda in file C_inlineFuntion.kt’ (注意加粗的黑體字的報錯)

但是如果hello()函數共有API,那麼就可以調用了

fun hello(){
    println("hello")
}

inline fun ff(){
    hello()  //内聯函數隻要調用共有的APi,就能編譯通過
}
           

⼀個 internal 聲明可以由 @PublishedApi 标注,這會允許它在公有 API 内聯函數中使用。當⼀個 internal 内聯函數标記有 @PublishedApi時,也會像公有函數⼀樣檢查其函數體。

internal fun hello(){
    println("hello")
}

inline fun ff(){
    hello()  //這樣調用也會編譯報錯,即使是internal子產品級的,也會編譯不通過。
}
           

但是下面的代碼會通過

@PublishedApi
internal fun hello(){
    println("hello")
}

inline fun ff(){
    //這裡的代碼不會報錯,hello()函數使用了@PublishedApi标記,hello()函數也會像公有函數⼀樣檢查其函數體
    hello() 
}