Kotlin 教程(二)

四、高階函數

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")
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章