Kotlin學習路(七):高階函數與內聯函數關係

<本文學習郭神《第三行代碼》總結>

定義用法

高階函數:如果一個函數接收另一個函數作爲參數,或者返回值的類型是另一個函數,那麼該函數稱爲高階函數。

語法規則:(String, Int)-> Unit
1、在->左邊的部分就是用來聲明該函數接收什麼參數,多個參數之間用逗號隔開,如果不接收任何參數,則用空括號,比如: ()-> Unit。
2、在右邊則聲明該函數返回的值類型,如果沒有返回值就使用Unit,它相當於void。

比如,將上述函數類型添加到某個函數的參數聲明或者返回值聲明上,那麼這個函數就是一個高階函數了:

fun example(func: (String, Int) -> Unit){
func("aaa", 123)
}

在這裏,example函數接收了一個函數類型的參數,因此example就是一個高階函數。

高階函數允許讓函數類型的參數來決定函數的執行邏輯。即使是同一個函數參數,那麼它的執行邏輯和最終的返回結果都可能是完全不同的。

例如:
定義一個方法num1AndNum2()的高階函數,並讓它接收兩個整型和一個函數類型參數,並最終返回運算結果。

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int{
    val result = operation(num1, num2)
    return result
}

num1AndNum2()函數的第三個參數是一個接收兩個整型參數,並有一個返回值的函數參數,這裏還需要定義兩個方法和上述函數參數類型匹配。

fun plus(num1: Int, num2: Int) : Int{
    return num1 + num2
}
fun minus(num1: Int, num2: Int) : Int{
    return num1 - num2
}

這兩個函數的參數返回類型和num1AndNum2()的函數參數類型返回完全一樣。
接下來開始使用這個方法:

fun main(){
    val num1 = 100
    val num2 = 10
    val result1 = num1AndNum2(num1, num2, :: plus)
    val result2 = num1AndNum2(num1, num2, :: minus)
    print("result1 is $result1")
    print("result2 is $result2")
}

這裏第三個參數使用了 :: plus、:: minus這種寫法,這是一種函數的引用方法,表示將plus和minus函數作爲參數傳遞給num1AndNum2()函數。

使用這種函數引用的寫法雖然能夠正常工作,但是每次調用時都還需先定義與之匹配的方法,會很繁瑣,所有,這裏可以替換成Lambda表達式的方法實現。

上述代碼就可以修改爲:

fun main(){
    val num1 = 100
    val num2 = 10
    val result1 = num1AndNum2(num1, num2){
        n1, n2 -> n1 + n2
    }
    val result2 = num1AndNum2(num1, num2){
        n1, n2 -> n1 - n2
    }

Lambda表達式提供一個指定的上下文,當需要連續調用同一個對象的多個方法時,apply函數就可以讓代碼更精簡,比如StringBuilder。
例如:

fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
	block()
	return this
}

這裏給StringBuilder定義一個build擴展函數,這個擴展函數接收一個函數參數,並且返回一個StringBuilder類型值。在函數前面加上ClassName. 就是表示這個函數類型是定義在哪個類中。

val list  = listOf("a", "b", "c")
val result = StringBuilder().build {
    for (s in list){
        append(s)
    }
}
print(s)

這裏build函數的用法和apply用法一樣,唯一區別就是build函數只是作用在StringBuilder上,而apply函數則是作用在所有類上,這就需要藉助Kotlin泛型纔行。

原理

現在知道高階函數怎麼用了,但是我們還需要知道它的原理。
還是用上述代碼爲例,調用num1AndNum2()函數,通過Lambda表達式傳入兩個整型參數,將代碼轉換成Java代碼則是:

public static int num1AndNum2(int num1, int num2, Function operation){
    int result = (int)operation.invoke(num1, num2);
    return result;
}
public static void main(){
    int num1 = 100;
    int num2 = 10;
    int result = num1AndNum2(num1, num2, new Function() {
        @Override
        public int invoke(Integer n1, Integer n2) {
            return n1 + n2;
        }
    });
}

在這裏num1AndNum2()函數的第三個參數變成了Function接口,這是Kotlin的內置接口,裏面待實現invoke()函數,在調用num1AndNum2()函數時,之前的Lambda表達式在這裏變成了Function接口的匿名實現類。

所以,每調用一次Lambda表達式,都會創建一個新的匿名類實例,這也會造成額外的內存和性能開銷。

inline

爲了解決這個問題,Kotlin提供了內聯函數,內聯函數的用法非常簡單,只需要在定義高階函數時加上inline關鍵字即可,上述代碼就可修改爲

inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int{
    val result = operation(num1, num2)
    return result
}

首先,Kotlin編譯器會將Lambda表達式中的代碼替換到函數類型參數調用的地方。
然後,再將內聯函數中的全部代碼替換到函數調用的地方。
最終,就會替換成兩個Int直接相加。

noinline

當一個高階函數接收了多個函數參數類型時,inline會自動將所有Lambda表達式全部進行內聯,如果只是想內聯期中一個,inline就不滿足,所以這裏需要用到noinline關鍵字。
比如:

inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit){

}

這裏使用了inline聲明inlineTest函數,原本block1、block2都會被內聯,但是在block2前加關鍵字noinline,那麼只會對block1內聯。

inline與oninline區別:

關鍵字inline:內聯的函數參數類型在編譯的時候回唄進行代碼替換,因此沒有真正的參數屬性,它所引用的表達式中可以使用return關鍵字進行函數返回。
關鍵字noinline:非內聯函數參數類型可以自由傳遞給其他任何函數,因爲它就是一個真實的參數,而內聯的函數參數只允許傳遞給另外一個內聯函數,它只能進行局部返回。
比如:

fun printString(str: String, block: (String) -> Unit){
    block(str)
}
fun main(){
    val str = ""
    printString(str){
        s -> 
        if (s.isEmpty())
            return@printString
        print(s)
 print("END")
    }
}

這裏定義一個printString的高階函數,用於在Lambda表達式中傳入打印的字符串,如果字符串參數爲空,則不打印。

在Lambda中不能直接使用return關鍵之,所以這裏return@printString表示局部返回,並且不執行後面的代碼,功能與Java中return一樣。

如果傳入的參數是一個空字符串,則不會執行return之後的語句。

但是如果將printString函數聲明成一個內聯函數,則可以再Lambda中使用return關鍵字。
比如:

inline fun printString(str: String, block: (String) -> Unit){
    block(str)
}
fun main(){
    val str = ""
    printString(str){
        s -> 
        if (s.isEmpty())
            return
        print(s)
 print("END")
    }
}

這裏return代表的是返回層的調用函數,也就是main函數。

crossinline

絕大多數高階函數可以聲明內聯函數,少部分是不行的。
比如:

inline fun runRunnable(block: () ->vUnit){
	val runnable = Runnable{
		block()
	}
	runnable.run()
}

上述代碼再沒有inline聲明是可以正常工作的,但是加上inline後會提示錯誤。

在runRunable函數中創建一個Runable對象,並在Runable的Lambda表達式中調用了傳入的函數參數。

而Lambda表達式在編譯的時候會被轉換成匿名類的實現方式,實際上上述代碼實在匿名類中調用了傳入的函數參數。

而內聯函數所引用的Lambda允許使用return進行函數返回,但是由於實在匿名類中調用的函數參數,所以不可能進行外層調用函數的返回,最多隻能對匿名類中的函數調用進行返回。

也就是說,在高階函數中創建Lambda或者匿名類的實現,並且在這些實現中調用函數參數,此時再將高階函數聲明成內聯函數,一定會報錯。

在這種情況下就必須使用crossinline關鍵字。
上述代碼就可修改爲:

inline fun runRunnable(crossinline block: () ->vUnit){
	val runnable = Runnable{
		block()
	}
	runnable.run()
}

這樣就可以正常編譯了。

因爲內聯函數的Lambda表達式可以使用return,但是高階函數的匿名類中不允許使用return,這就會導致衝突,而crossinline就可以解決這種衝突。
在聲明瞭crossinline後,就無法調用runRunable函數時的Lambda表達式中使用return進行函數返回了,但是仍然可以使用return@runRunable的方式進行局部返回。

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