Kotlin 語言學習(11) - 內聯函數

Kotlin 語言學習(1) - Kotlin 基礎

Kotlin 語言學習(2) - 函數的定義與調用

Kotlin 語言學習(3) - 類、對象和接口

Kotlin 語言學習(4) - 數據類、類委託 及 object 關鍵字

Kotlin 語言學習(5) - lambda 表達式和成員引用

Kotlin 語言學習(6) - Kotlin 的可空性

Kotlin 語言學習(7) - Kotlin 的類型系統

Kotlin 語言學習(8) - 運算符重載及其他約定

Kotlin 語言學習(9) - 委託屬性

Kotlin 語言學習(10) - 高階函數:Lambda

Kotlin 語言學習(11) - 內聯函數

Kotlin 語言學習(12) - 泛型類型參數

一、本文概要

二、內聯函數

當我們使用lambda表達式時,它會被正常地編譯成匿名類。這表示每調用一次lambda表達式,一個額外的類就會被創建,並且如果lambda捕捉了某個變量,那麼每次調用的時候都會創建一個新的對象,這會帶來運行時的額外開銷,導致使用lambda比使用一個直接執行相同代碼的函數效率更低。

如果使用inline修飾符標記一個函數,在函數被調用的時候編譯器並不會生成函數調用的代碼,而是 使用函數實現的真實代碼替換每一次的函數調用

2.1 內聯函數如何運作

當一個函數被聲明爲inline時,它的函數體是內聯的,也就是說,函數體會被直接替換到函數被調用地方,下面我們來看一個簡單的例子,下面是我們定義的一個內聯的函數:

inline fun inlineFunc(prefix : String, action : () -> Unit) {
    println("call before $prefix")
    action()
    println("call after $prefix")
}

我們用如下的方法來使用這個內聯函數:

fun main(args: Array<String>) {
    inlineFunc("inlineFunc") {
        println("HaHa")
    }
}

運行結果爲:

>> call before inlineFunc
>> HaHa
>> call after inlineFunc

最終它會被編譯成下面的字節碼:

fun main(args: Array<String>) {
    println("call before inlineFunc")
    println("HaHa")
    println("call after inlineFunc")
}

lambda表達式和inlineFunc的實現部分都被內聯了,由lambda生成的字節碼成了函數調用者定義的一部分,而不是被包含在一個實現了函數接口的匿名類中。

傳遞函數類型的變量作爲參數

在調用內聯函數的時候,也可以傳遞函數類型的變量作爲參數,還是上面的例子,我們換一種調用方式:

fun main(args: Array<String>) {
    val call : () -> Unit = { println("HaHa") }
    inlineFunc("inlineFunc", call)
}

那麼此時最終被編譯成的Java字節碼爲:

fun main(args: Array<String>) {
    println("call before inlineFunc ")
    action()
    println("call after inlineFunc")
}

在這種情況,只有inlineFunc的實現部分被內聯了,而lambda的代碼在內聯函數被調用點是不可用的。

在兩個不同的位置使用同一個內聯函數

如果在兩個不同的位置使用同一個內聯函數,但是用的是不同的lambda,那麼內聯函數會在每一個被調用的位置分別內聯,內聯函數的代碼會被拷貝到使用它的兩個不同位置,並把不同的lambda替換到其中。

2.2 內聯函數的限制

鑑於內聯的運作方式,不是所有使用 lambda 的函數都可以被內聯。當函數被內聯的時候,作爲參數的lambda表達式的函數體會被 替換到最終生成的代碼中

這將限制函數體中的lambda參數的使用:

  • 如果lambda參數 被調用,這樣的代碼能被容易地內聯。
  • 如果lambda參數 在某個地方被保存起來,以便以後繼續使用,lambda表達式的代碼 將不能被內聯,因此必須要 有一個包含這些代碼的對象存在

一般來說,參數如果 被直接調用或者作爲參數傳遞 給另外一個inline函數,它是可以被內聯的,否則,編譯器會 禁止參數被內聯 並給出錯誤信息Illeagal usage of inline-parameter

例如,許多作用於序列的函數會返回一些類的實例,這些類代表對應的序列操作並接收lambda作爲構造方法的參數,以下是Sequence.map函數的定義:

fun <T, R> Sequence<T>.map(transform : (T) -> R) : Sequence<R> {
    return TransformingSequence(this, transform);
}

map函數沒有直接調用作爲transform參數傳遞進來的函數。而是將這個函數傳遞給一個類的構造方法,構造方法將它保存在一個屬性當中。爲了支持這一點,作爲transform參數傳遞的lambda需要 被編譯成標準的非內聯表示法,即一個實現了函數接口的匿名類。

如果一個函數期望兩個或更多的lambda函數,可以選擇只內聯其中一些參數,因爲一個lambda可能會包含很多代碼或者 以不允許內聯的方式調用,接收這樣的非內聯lambda的參數,可以用noinline修飾符來標記它:

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

}

注意,編譯器完全支持 內聯跨模塊的函數或者第三方庫定義的函數,也可以在 Java 中調用絕大部分內聯函數

2.3 內聯集合操作

大部分標準庫中的集合函數都帶有lambda參數。例如filter,它被聲明爲內聯函數,這意味着filter函數,以及傳遞給它的lambda字節碼會被內聯到filter被調用的地方,因此我們不用擔心性能問題。

假如我們像下面這樣,連續調用filtermap兩個操作:

println(people.filter{ it.age > 30 }.map(Person :: name))

這個例子使用了一個lambda表達式和一個成員引用,filtermap函數都被聲明爲inline函數,所以不會額外產生類或者對象,但是上面的代碼會創建一箇中間集合來保存列表過濾的結果。

2.4 決定何時將函數聲明成內聯

對於普通函數的調用,JVM已經提供了強大的內聯支持。它會分析代碼的執行,並在任何通過內聯能夠帶來好處的時候將函數調用內聯。

帶有lambda參數的函數內聯能帶來好處:

  • 節約了函數調用的開銷,節約了爲lambda創建匿名類,以及創建lambda實例對象的開銷。
  • JVM目前並沒有聰明到總是能夠將函數調用內聯。
  • 內聯使得我們可以使用一些不可能被普通lambda使用的特性,例如 非局部返回

但是在使用inline關鍵字的時候,還是應該注意代碼的長度,如果你要內聯的函數很大,將它的字節碼拷貝到每一個調用點將會極大地增加字節碼的長度。在這種情況下,你應該將那些與lambda參數無關的代碼抽取到一個獨立的非內聯函數中。

三、高階函數中的控制流

當你使用lambda去替換像循環這樣的命令式代碼結構時,很快就會遇到return表達式的問題,把一個return語句放在循環的中間是很簡單的事。但是如果將循環替換成一個類似filter的函數呢?

3.1 lambda 中的返回語句:從一個封閉的函數返回

下面,我們通過一個例子來演示,在集合當中尋找名爲Alice的人,找到了就直接返回:

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

運行結果爲:

>> Found !

如果在lambda中使用return關鍵字,它會 從調用 lambda 的函數 中返回,並不只是 從 lambda 中返回,這樣的return語句叫做 非局部返回,因爲它從一個比包含return的代碼塊更大的代碼塊中返回了。

需要注意的是,只有 以 lambda 作爲參數的函數是內聯函數 的時候才能從更外層的函數返回。在一個非內聯的lambda中使用return表達式是不允許的,一個非內聯函數可以把它的lambda保存在變量中,以便在函數返回以後可以繼續使用,這個時候lambda想要去影響函數的返回已經太晚了。

3.2 從 lambda 中返回:使用標籤返回

也可以在lambda表達式中使用局部返回,類似於for循環中的break表達式,它會終止lambda的執行,並接着從調用lambda的代碼處執行。

要區分局部返回和非局部返回,要用到標籤。想從一個lambda表達式處返回你可以標記它,然後在return關鍵字後面引用這個標籤。

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach label@{
        if (it.name == "Alice") return@label
    }
    println("Alice might be somewhere")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

運行結果爲:

>> Alice might be somewhere

另一種選擇是,使用lambda作爲參數的函數的函數名可以作爲標籤,也就是上面的forEach,如果你顯示地指定了lambda表達式的標籤,再使用函數名作爲標籤沒有任何效果。

3.3 匿名函數:默認使用局部返回

匿名函數是一種不同的用於編寫傳遞給函數的代碼塊的方式,先來看一個示例:

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach(fun (person) {
        if (person.name == "Alice") return
        println("${person.name} is not Alice")
    })
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

運行結果爲:

>> Bob is not Alice

匿名函數和普通函數有相同的指定返回值類型的規則,代碼塊匿名函數 需要顯示地指定返回類型,如果使用 表達式函數體,就可以省略返回類型。

在匿名函數中,不帶return表達式會從匿名函數返回,而不是從包含匿名函數的函數返回,這條規則很簡單:return從最近的使用fun關鍵字聲明的函數返回。

  • lambda表達式沒有使用fun關鍵字,所以lambda中的return從最外層的函數返回。
  • 匿名函數使用了fun,因此return表達式從匿名函數返回。

儘管匿名函數看起來和普通函數很相似,但它其實是lambda表達式的另一種語法形式而已。關於lambda表達式如何實現,以及在內聯函數中如何被內聯的討論同樣適用於匿名函數。

see you

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章