天天看點

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())
            [email protected]
        print(s)
 print("END")
    }
}
           

這裡定義一個printString的高階函數,用于在Lambda表達式中傳入列印的字元串,如果字元串參數為空,則不列印。

在Lambda中不能直接使用return關鍵之,是以這裡[email protected]表示局部傳回,并且不執行後面的代碼,功能與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進行函數傳回了,但是仍然可以使用[email protected]的方式進行局部傳回。