Kotlin知識歸納(十一) —— 高階函式

大棋發表於2019-07-09

前序

      之前已經掌握了函式型別的定義以及lambda的使用,本次將完成高階函式與行內函數的學習。

高階函式就是以另一函式作為引數或返回值的函式。

函式型別

函式型別的原理

      眾所周知,Kotlin是相容Java 6的,但Java 6並沒有 lambda 。所以Kotlin會將一個函式型別的變數轉換為一個FunctionN介面的實現。

      Kotlin標準庫中定義了一系列FunctionN介面,這些介面對應於不同引數數量的函式:Function0<R>代表沒有引數的函式,Function1<P1,R>代表一個引數的函式。N的取值範圍: 0 <= N <= 22,也就是說函式型別變數的引數最多22個。

Kotlin知識歸納(十一) —— 高階函式

public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}
複製程式碼

      每一個FunctionN介面都定義了一個invoke方法,一個函式型別的變數就是實現了對應FunctionN介面的例項,該 FunctionN介面的例項 invoke 方法中包含了函式型別變數的函式體。

函式型別例項的呼叫

      細心點會發現,每一個FunctionN函式中的invoke()都帶有operator關鍵字修飾,這表明invoke()的呼叫可以用操作符替代:

表示式 轉換
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ……, i_n) a.invoke(i_1, ……, i_n)

函式型別變數可以使用圓括號替代呼叫 invoke(),但還是需要傳遞足夠數量的引數:

#daqiKotlin.kt
fun doSometing(func :() -> Unit){
    func()
    //func.invoke()
}
複製程式碼

對於接收函式型別引數的函式,Java 8可以直接使用lambda進行呼叫,並且lambda會被自動轉換成函式型別的值:

#daqiJava8.java
doSometing{
    
}
複製程式碼

而對於Java 8以下的Java,需要按照函式型別的引數列表數量,選取適當的FunctionN介面,建立其匿名內部類。

#daqiJava.java
doSometing(new Function0<Unit>() {
    @Override
    public Unit invoke() {
        return Unit.INSTANCE;
    }
});
複製程式碼

返回函式的函式

      函式型別可以用作引數傳遞到函式中,也就意味著函式型別也能作為返回值返回,雖然這並不常用。

fun  doSometing():() -> Unit{
    return {
        
    }
}
複製程式碼

行內函數

lambda的開銷

      Kotlin的lambda帶來簡潔語法的同時,也帶來了一定的效能消耗。每一個lambda表示式會被編譯成一個實現FunctionN介面的匿名類。

      對於捕捉變數的lambda表示式,每次呼叫都是建立一個新的物件,帶來額外的效能開銷。對於不捕捉變數的lambda表示式,只會建立一個單例的 FunctionN 例項,並在下次呼叫時複用。

      同時 Kotnlin 編譯出來的 Function 物件沒有避免對基本資料型別的裝箱和拆箱(因為接收的是Object型別)。這就意味著輸入值或輸出值涉及到基本資料型別時,會呼叫系統的裝箱與拆箱,造成一定的效能消耗。

(Function1)(new Function1() {

     public Object invoke(Object var1) {
        this.invoke(((Number)var1).intValue());
        return Unit.INSTANCE;
     }

     public final void invoke(int it) {
       //....
     }
})
複製程式碼

      為了生成與Java語句一樣高效的程式碼,Kotlin提供了內聯機制。對於帶有inline修飾符函式,Kotlin編譯器會直接將函式實現的真實程式碼替換每一次的函式被呼叫的地方。

      內聯的 lambda 表示式只能在行內函數內部直接呼叫或者作為可內聯的引數傳遞。否則,編譯器會禁止引數被內聯,並給出錯誤資訊“Illegal usage of inline-parameter”。而且內聯的函式應儘量簡單,比如Kotlin標準庫中的行內函數總是很小的。

禁用內聯

      當一個lambda可能會包含很多程式碼,或者以不允許內聯的方式使用,那麼可以用 noinline 修飾符標記這些不希望內聯的函式型別引數:

inline fun daqi(noinline func :() -> Unit){
    func()
}
複製程式碼

      如果一個行內函數沒有可內聯的函式型別引數,編譯器會產生一個警告,因為這樣的行內函數沒意義。noinline 修飾的函式型別引數可以以任何方式操作。例如儲存在欄位中等等。

集合的內聯操作

      集合的函式式Api操作都是行內函數,所以它們的函式體都會被內聯,不會產生額外的類或者物件。

#_Collections.kt
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}
複製程式碼

      但序列的函式式Api中,中間操作並不是行內函數(末端操作是行內函數),所以中間操作的lambda都會被儲存在欄位中,等待末端操作呼叫由中間操作組成的呼叫鏈。

#_Sequences.kt
public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
    return FilteringSequence(this, true, predicate)
}

internal class FilteringSequence<T>(
    private val sequence: Sequence<T>,
    private val sendWhen: Boolean = true,
    private val predicate: (T) -> Boolean
) : Sequence<T> {
}
複製程式碼

      所以,不應該總是試圖將集合轉化為序列,只有在處理大量資料的集合時,才需要將集合轉換為序列。對於小資料量的集合,還是使用集合操作處理。

參考資料:

android Kotlin系列:

Kotlin知識歸納(一) —— 基礎語法

Kotlin知識歸納(二) —— 讓函式更好呼叫

Kotlin知識歸納(三) —— 頂層成員與擴充套件

Kotlin知識歸納(四) —— 介面和類

Kotlin知識歸納(五) —— Lambda

Kotlin知識歸納(六) —— 型別系統

Kotlin知識歸納(七) —— 集合

Kotlin知識歸納(八) —— 序列

Kotlin知識歸納(九) —— 約定

Kotlin知識歸納(十) —— 委託

Kotlin知識歸納(十一) —— 高階函式

Kotlin知識歸納(十二) —— 泛型

Kotlin知識歸納(十三) —— 註解

Kotlin知識歸納(十四) —— 反射

Kotlin知識歸納(十一) —— 高階函式

相關文章