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