<本文學習郭神《第三行代碼》總結>
定義用法
高階函數:如果一個函數接收另一個函數作為參數,或者傳回值的類型是另一個函數,那麼該函數稱為高階函數。
文法規則:(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]的方式進行局部傳回。