四、高階函數
4.1 匿名函數
高階函數的意思是使用函數作爲變量或者返回值的函數。
// 這就是一個高階函數,它的參數是一個函數類型的對象
// (Int) -> Unit 表示這是一個參數爲 Int,無返回值的函數類型的對象
fun high(function: (Int) -> Unit) {
// 函數類型的對象通過 invoke 來調用其函數
function.invoke(666)
}
fun main() {
// 調用高階函數時傳入一個函數即可,傳入的函數沒有名字,所以又被稱之爲匿名函數
high(fun(number: Int): Unit {
println("number = $number")
})
}
4.2 Lambda 表達式
fun high(function: (Int) -> Unit) {
function.invoke(666)
}
fun main() {
// 傳入的匿名函數可以簡化爲 Lambda 表達式的形式
high({ number: Int ->
println("number = $number")
})
// 如果 Lambda 表達式是高階函數的最後一個參數,則可以把 Lambda 表達式寫在括號外面
high() { number: Int ->
println("number = $number")
}
// 如果 Lambda 表達式是高階函數的唯一參數,則可以省略括號
high { number: Int ->
println("number = $number")
}
// 如果 傳入的這個參數是單參數的,則可以省略不寫這個參數,使用默認名字 it 表示此參數即可
high {
println("number = $it")
}
}
Kotlin 的匿名函數和 Lambda 表達式本質上就是一個函數類型的對象,它和函數不是同一個東西,而是一個 Kotlin 幫我們自動生成的,我們看不見的一個和函數具有相同功能的對象。
4.3 雙冒號 + 函數名
除了使用匿名函數和 Lambda 之外,我們還可以通過雙冒號 “::” + 函數名的方式來構造一個函數類型的對象。
fun high(function: (Int) -> Unit) {
function.invoke(666)
}
fun main() {
// “::” + 函數名可以使函數變成函數類型的對象,使得它可以被傳入高階函數中
high(::normal)
}
fun normal(number: Int) {
println("number = $number")
}
五、內聯函數
5.1 inline
函數類型的對象說起來比較抽象,如果用 Java 來表示的話,高階函數的代碼大致是這樣的:
public static void high(Function function) {
function.invoke(666);
}
public static void main() {
// Kotlin 內部會幫我們建立一個和函數功能相同的對象,這個對象就被稱之爲函數類型的對象
high(new Function() {
@Override
public void invoke(int number) {
System.out.println("number = " + number);
}
});
}
也就是說,每當我們使用函數類型的對象時,都會創建一個新的對象,這就會造成額外的內存和性能開銷。爲了解決此問題,Kotlin 提供了內聯函數。
// 添加 inline 關鍵字使其變成內聯函數
inline fun high(function: (Int) -> Unit) {
function.invoke(666)
}
fun main() {
// 在編譯時,Kotlin 首先會將內聯函數中 Lambda 表達式的內容替換到其調用的地方,然後再將內聯函數中的全部代碼移到調用處
// 也就是說,這裏最終的代碼會變成直接調用 println("number = 666"),所以說內聯函數完全消除了 Lambda 表達式帶來的運行時開銷
high {
println("number = $it")
}
}
如果給非高階函數添加 inline 關鍵字,我們會看到一條提示:Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
,意思是 inline 對普通函數的性能提升微乎其微,inline 關鍵字主要用於高階函數。這是因爲 JVM 已經對普通的函數進行了內聯優化。
5.2 noinline
如果一個高階函數接收了兩個或者更多的函數類型的對象作爲參數,在這個高階函數添加了 inline 關鍵字後,所有函數類型的對象均會被內聯,如果我們想要某個函數類型的對象不被內聯,則可以爲此參數添加 noinline 關鍵字。
// 給 block1 參數添加 noinline 關鍵字使其不要內聯
inline fun high(noinline block1: () -> Unit, block2: () -> Unit){}
既然內聯函數這麼好用,爲什麼還要提供 noinline 呢?
上文中說到,內聯函數在編譯時會被替換爲實際運行的代碼,所以說它的參數沒有真正的參數屬性,在函數內部需要傳參時,只能傳遞給另一個內聯函數,這就是它最大的侷限性。而非內聯函數的參數是真實的參數,可以自由的進行傳遞。
5.3 crossinline
在 Kotlin 的 Lambda 表達式中,return 只能用於局部返回,並且加上 @返回範圍
:
thread {
// 這裏是局部返回,終止 thread 中的代碼,但 thread 之後的代碼不受影響
return@thread
}
// thread 之後的代碼可以繼續執行
但由於內聯函數中的 Lambda 會在編譯時替換爲實際代碼,所以內聯函數的 Lambda 中,可以使用 return 全局返回:
inline fun high(block: () -> Unit) {
block.invoke()
}
fun main() {
high {
println("hello")
// 這裏是全局返回,Lambda 之後的代碼無法再被運行。(這裏也可以用 return@high 局部返回)
return
}
// 這裏的代碼不會被執行
println("test")
}
如果內聯函數中的 Lambda 表達式在函數內另一個 Lambda 表達式中使用,return 就可能產生歧義:
inline fun high(block: () -> Unit) {
thread {
// 這裏是無法編譯通過的,因爲這裏會產生歧義
// 如果 block 的代碼中使用了 return,想要進行全局返回,但由於 block 的代碼是在這裏的 Lambda 表達式中執行的,無法全局返回,就會導致出錯。
block.invoke()
}
}
爲了解決這個問題,我們需要給 block 參數加上 crossinline 關鍵字,禁止 block 的代碼中使用全局返回:
// 爲 block 參數添加 crossinline 關鍵字,表示不允許 block 中使用全局返回。
inline fun high(crossinline block: () -> Unit) {
thread {
block.invoke()
}
}
fun main() {
high {
println("hello")
// 由於這裏的 Lambda 加上了 crossinline 關鍵字,所以這裏無法再使用全局返回,只能局部返回
return@high
}
// 這裏的代碼依舊能夠執行
println("test")
}