本文是對<<Kotlin in Action>>
的學習筆記,若是須要運行相應的代碼能夠訪問在線環境 try.kotlinlang.org,這部分的思惟導圖爲: java
當咱們使用lambda
表達式時,它會被正常地編譯成匿名類。這表示每調用一次lambda
表達式,一個額外的類就會被建立,而且若是lambda
捕捉了某個變量,那麼每次調用的時候都會建立一個新的對象,這會帶來運行時的額外開銷,致使使用lambda
比使用一個直接執行相同代碼的函數效率更低。函數
若是使用inline
修飾符標記一個函數,在函數被調用的時候編譯器並不會生成函數調用的代碼,而是 使用函數實現的真實代碼替換每一次的函數調用。性能
當一個函數被聲明爲inline
時,它的函數體是內聯的,也就是說,函數體會被直接替換到函數被調用地方,下面咱們來看一個簡單的例子,下面是咱們定義的一個內聯的函數:學習
inline fun inlineFunc(prefix : String, action : () -> Unit) {
println("call before $prefix")
action()
println("call after $prefix")
}
複製代碼
咱們用以下的方法來使用這個內聯函數:this
fun main(args: Array<String>) {
inlineFunc("inlineFunc") {
println("HaHa")
}
}
複製代碼
運行結果爲:spa
>> call before inlineFunc
>> HaHa
>> call after inlineFunc
複製代碼
最終它會被編譯成下面的字節碼:code
fun main(args: Array<String>) {
println("call before inlineFunc")
println("HaHa")
println("call after inlineFunc")
}
複製代碼
lambda
表達式和inlineFunc
的實現部分都被內聯了,由lambda
生成的字節碼成了函數調用者定義的一部分,而不是被包含在一個實現了函數接口的匿名類中。orm
在調用內聯函數的時候,也能夠傳遞函數類型的變量做爲參數,仍是上面的例子,咱們換一種調用方式:cdn
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
替換到其中。
鑑於內聯的運做方式,不是全部使用 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 中調用絕大部份內聯函數。
大部分標準庫中的集合函數都帶有lambda
參數。例如filter
,它被聲明爲內聯函數,這意味着filter
函數,以及傳遞給它的lambda
字節碼會被內聯到filter
被調用的地方,所以咱們不用擔憂性能問題。
假如咱們像下面這樣,連續調用filter
和map
兩個操做:
println(people.filter{ it.age > 30 }.map(Person :: name))
複製代碼
這個例子使用了一個lambda
表達式和一個成員引用,filter
和map
函數都被聲明爲inline
函數,因此不會額外產生類或者對象,可是上面的代碼會建立一箇中間集合來保存列表過濾的結果。
對於普通函數的調用,JVM
已經提供了強大的內聯支持。它會分析代碼的執行,並在任何經過內聯可以帶來好處的時候將函數調用內聯。
帶有lambda
參數的函數內聯能帶來好處:
lambda
建立匿名類,以及建立lambda
實例對象的開銷。JVM
目前並無聰明到老是可以將函數調用內聯。lambda
使用的特性,例如 非局部返回。可是在使用inline
關鍵字的時候,仍是應該注意代碼的長度,若是你要內聯的函數很大,將它的字節碼拷貝到每個調用點將會極大地增長字節碼的長度。在這種狀況下,你應該將那些與lambda
參數無關的代碼抽取到一個獨立的非內聯函數中。
當你使用lambda
去替換像循環這樣的命令式代碼結構時,很快就會遇到return
表達式的問題,把一個return
語句放在循環的中間是很簡單的事。可是若是將循環替換成一個相似filter
的函數呢?
下面,咱們經過一個例子來演示,在集合當中尋找名爲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
想要去影響函數的返回已經太晚了。
也能夠在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
表達式的標籤,再使用函數名做爲標籤沒有任何效果。
匿名函數是一種不一樣的用於編寫傳遞給函數的代碼塊的方式,先來看一個示例:
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
表達式如何實現,以及在內聯函數中如何被內聯的討論一樣適用於匿名函數。