kotlin - 擴展函數、高階函數、內聯函數

關鍵詞

  • 擴展函數
  • 高階函數
  • 內聯函數

在上篇文章 偷師 - Kotlin 委託 裏提到了 ViewBindingDelegate 庫,經過 kotlin 委託的方式簡化了在 Android 項目中 ViewBinding 使用。原本是不想再寫 ViewBindingDelegate 分析的,可是項目中用到的 kotlin 知識點確實也有些是須要重點記錄一下的。java

直接來看 vbpd-full/../ActivityViewBindings.kt 文件中的 viewBinding 方法:web

...

@JvmName("inflateViewBindingActivity")
public inline fun <reified T : ViewBinding> ComponentActivity.viewBinding(
    createMethod: CreateMethod = CreateMethod.BIND
) = viewBinding(T::class.java, createMethod)

...
複製代碼

能夠看到這是一個擴展函數。第一個知識點來了!markdown

擴展函數

擴展函數定義:不改變原有類的狀況下,擴展新的功能ide

首先肯定一點擴展函數針對的是類,爲類提供新的功能。那是怎麼實現的呢,看下面的示例:函數

我定義了一個 String 的擴展函數用以輸出它的長度:post

private fun String.printLength() {
}
複製代碼

轉換成 java 代碼看下性能

private final void printLength(String $this$printLength) {
}
複製代碼

這樣就很好的理解擴展函數的本質了:擴展函數的本質就是一個普通的函數,它不會對原有類作任何修改,不同的地方在於它默認以類對象做爲函數的參數學習

在擴展函數內部你能夠經過 this 關鍵字訪問傳過來的在點符號前的對象,也就是上面示例中的 $this$printLength 參數。而 this 也能夠省略。優化

private fun String.printLength() {
    Log.e("length", "$length")
}
複製代碼

轉換爲 java 代碼以下:this

private final void printLength(String $this$printLength) {
  Log.e("length", String.valueOf($this$printLength.length()));
}
複製代碼
val name = "張三"
name.printLength()
複製代碼

輸出爲:2。

這也就是爲何在 ViewBindingDelegate 項目中會忽然出現 activity 變量的緣由:

@JvmName("viewBindingActivity")
public fun <T : ViewBinding> ComponentActivity.viewBinding(
    viewBindingClass: Class<T>,
    rootViewProvider: (ComponentActivity) -> View
): ViewBindingProperty<ComponentActivity, T> {
    return viewBinding { activity -> ViewBindingCache.getBind(viewBindingClass).bind(rootViewProvider(activity)) }
}
複製代碼

總結

擴展函數和普通函數的區別:

  • 形式上:擴展函數比普通函數多了被擴展的類型做爲前綴 被擴展類型.函數名()
  • 用法上:擴展函數以被擴展的目標類做爲首參類型,此參數不可見,但可經過 this(可省略) 關鍵字訪問被擴展的目標類對象。

內聯函數

仍是上面的代碼:

@JvmName("inflateViewBindingActivity")
public inline fun <reified T : ViewBinding> ComponentActivity.viewBinding(
    createMethod: CreateMethod = CreateMethod.BIND
) = viewBinding(T::class.java, createMethod)
複製代碼

發現兩個不太認識的關鍵字:inlinereified。在介紹它們以前應該先理解兩個概念:

  • 高階函數:能夠將函數用做參數或返回值的函數。
  • 內聯函數:使用 inline 修飾的函數。能夠消除使用高階函數時所帶來的資源消耗。

高階函數

先看一個正常的函數,兩數相加:

private fun addTwoNumbers(firstNumber: Int, secondNumber: Int): Int {
    return firstNumber + secondNumber
}
複製代碼

很簡單,沒有什麼好說的。可是發現一個問題,若是計算兩數相減、相乘、相除就須要再定義三個函數,可是並不想這麼作,怎麼辦呢。這種狀況下高階函數就能夠派上用場了,新函數以下:

private fun calculateTwoNumber(
    firstNumber: Int,
    secondNumber: Int,
    calculate: (Int, Int) -> Int
): Int {
    return calculate(firstNumber, secondNumber)
}
複製代碼

函數 calculateTwoNumber 接受一個函數參數 calculatecalculate 函數接受兩個 Int 型參數並返回 Int 型結果。calculateTwoNumber 就是一個高階函數。

轉成 java 來看下:

private final int calculateTwoNumber(int firstNumber, int secondNumber, Function2 calculate) {
  return ((Number)calculate.invoke(firstNumber, secondNumber)).intValue();
}
複製代碼

發現 calculate 的參數類型是 Function2,既然有了 Function2 那是否是還有 Function四、五、六、七、八、9 呢。這個能夠有,事實上有 Function0~2223 個接口類型。

public interface Function<out R>

public interface Function0<out R> : Function<R> {
    public operator fun invoke(): R
}

public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}

/** 接收兩個參數的 Function */
public interface Function2<in P1, in P2, out R> : Function<R> {
    /** 執行 invoke 函數 經過參數 P一、P2 獲得並返回結果 R */
    public operator fun invoke(p1: P1, p2: P2): R
}

...

public interface Function10<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, out R> : Function<R> {
    public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10): R
}

...
複製代碼

它們的區別就是傳參數量的區別。

那如今如今清楚了所謂的高階函數其實編譯成 java 就是 具備 Function 參數類型或返回值爲 Function 類型的函數

kotlin 準備了兩種方式能夠得到 Function 對象:

  • lambda 表達式;
  • 匿名函數;

lambda 表達式語法以下:

{參數聲明 -> 函數體}

使用 lambda 注意如下幾點:

  1. 參數聲明類型可選,也就是說能夠不標註參數類型。
{a: Int, b: Int -> a + b}
等價於
{a, b -> a + b}
複製代碼
  1. 若是高階函數的最後一個參數是函數,那麼做爲相應參數傳入的 lambda 表達式能夠放在圓括號以外。
calculateTwoNumber(1, 2, {a: Int, b: Int -> a + b})
等價於
calculateTwoNumber(1, 2) { a: Int, b: Int -> a + b }
複製代碼
  1. 若是該 lambda 表達式是調用時惟一的參數,那麼圓括號能夠徹底省略。
run { println("...") }
複製代碼
  1. lambda 表達式只有一個參數時能夠不用聲明惟一的參數並忽略 ->
val ints = listOf<Int>()
ints.filter { it > 0 }
複製代碼
  1. lambda 表達式默認返回最後一個表達是的值,也能夠經過 return 顯示指定返回值。
ints.filter {
    val shouldFilter = it > 0 
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0 
    return@filter shouldFilter
}
複製代碼
  1. lambda 表達式中不可直接使用 return,要退出 lambda 須要用到標籤。可是若是傳給的函數是內聯(內聯函數在下文講解)的,能夠直接使用 return
fun ordinaryFunction(block: () -> Unit) {
    println("hi!")
}
fun foo() {
    ordinaryFunction {
        return // 錯誤:不能使 `foo` 在此處返回
        return@ordinaryFunction // 正確
    }
}
fun main() {
    foo()
}
複製代碼

lambda 表達式語法缺乏指定函數的返回類型的能力。在大多數狀況下返回類型能夠自動推斷出來。可是若是確實須要顯式指定,那就須要用到 匿名函數 了。

匿名函數和常規函數的區別在於匿名函數沒有函數名。其餘和常規函數如出一轍。

fun(x: Int, y: Int): Int {
    return x + y
}
複製代碼

若是函數返回類型能夠推導出來那麼返回類型也能夠省略。

高階函數優化 - 內聯函數

如今已經清楚了高階函數的定義,那咱們來用高階函數來計算 0~10 的和:

var result = 0
for (i in 0..10) {
    result = calculateTwoNumber(result, i) { a: Int, b: Int -> a + b }
}
Log.e("highfun", "$result") // 55
複製代碼

完美!結果明顯是正確的。那再看一下編譯後的代碼:

int result = 0;
int i = 0;

for(byte var4 = 10; i <= var4; ++i) {
 result = this.calculateTwoNumber(result, i, (Function2)null.INSTANCE);
}

Log.e("highfun", String.valueOf(result));
複製代碼

能夠發現每次循環都會建立 Function 實例,這樣在大量循環狀況下會產生大量對象,影響內存,這明顯得優化。優化方式有兩種:

優化一:將 lambda 放到循環外定義。

val addCalculate = { a: Int, b: Int -> a + b }
var result = 0
for (i in 0..10) {
    result = calculateTwoNumber(result, i, addCalculate)
}
複製代碼

優化二:使用 inline 修飾高階函數爲內聯函數。

private inline fun calculateTwoNumber(
    firstNumber: Int,
    secondNumber: Int,
    calculate: (Int, Int) -> Int
): Int {
    return calculate(firstNumber, secondNumber)
}
複製代碼

使用 inline 修飾高階函數後查看編碼以後的代碼:

for(byte var4 = 10; i <= var4; ++i) {
 int $i$f$calculateTwoNumber = false;
 int var9 = false;
 result += i;
}
複製代碼

能夠發現 lambda 表達式的函數體被添加到了表達式被調用的地方,從而避免了建立 Function 對象。

注意:內聯雖然會提高性能,但同時也會致使生成的代碼增長,因此應避免內聯過大的函數

noinline、crossinline

在上文中提到過傳遞給 inline 內聯函數的 lambda 表達式中可使用 return 返回。那咱們來看下面的例子:

private fun callFunction() {
    inlined {
        Log.e("inline", "2")
        return
    }
}

private inline fun inlined(body: () -> Unit) {
    Log.e("inline", "1")
    body()
    Log.e("inline", "3")
}

輸出 ------
E/inline: 1
E/inline: 2
複製代碼

能夠看到代碼中有三條日誌打印信息,可是輸出中只打印了兩條。這裏 return 在輸出最後一條日誌信息時直接結束了函數。因此使用 inline 內聯函數時應該避免直接使用 return,而改用 return@標籤 的方式。修改下代碼:

private fun callFunction() {
    inlined {
        Log.e("inline", "2")
        return@inlined
    }
}

輸出 ------
E/inline: 1
E/inline: 2
E/inline: 3
複製代碼

kotlin 中也提供了兩個修飾符來幫助限制在 lambda 中直接使用 return

  • noinline
  • crossinline

noinline 若是但願只內聯一部分傳給內聯函數的 lambda 表達式參數,那麼能夠用 noinline 修飾符標記不但願內聯的函數參數:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { …… }
複製代碼

能夠內聯的 lambda 表達式只能在內聯函數內部調用或者做爲可內聯的參數傳遞,可是 noinline 的能夠以任何咱們喜歡的方式操做:賦值給變量傳遞給其餘高階函數 等等。

並且使用 noinline 修飾的函數參數,在爲其傳遞 lambda 表達式時不能直接使用 return 否則會報錯,須要使用 return@標籤。修改上面的示例:

private inline fun inlined(noinline body: () -> Unit) {
    Log.e("inline", "1")
    body()
    Log.e("inline", "3")
}

輸出 ------
E/inline: 1
E/inline: 2
E/inline: 3
複製代碼

可是使用 noinline 也會出現一個問題,咱們看一下編譯後的 java 代碼:

private final void callFunction() {
  Function0 body$iv = (Function0)null.INSTANCE;
  int $i$f$inlined = false;
  Log.e("inline", "1");
  body$iv.invoke();
  Log.e("inline", "3");
}
複製代碼

看起來跟沒有使用 inline 修飾的高階函數調用是如出一轍的。並且你會看到警告信息:

Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types 意思就是若是一個內聯函數沒有可內聯的函數參數而且沒有具體化的類型參數,那麼這樣的函數極可能並沒有益處(若是你確認須要內聯,則能夠用 @Suppress("NOTHING_TO_INLINE") 註解關掉該警告)。

那有沒有便可以函數內聯還能夠保證 lanmbda 傳參裏沒有直接使用 return 呢。

crossinline crossinlinenoinline 均可以限制 lambda 傳參不可直接使用 return,區別在於 crossinline 修飾的函數參數仍然是內聯的。修改上面的示例:

private inline fun inlined(crossinline body: () -> Unit) {
    Log.e("inline", "1")
    body()
    Log.e("inline", "3")
}
複製代碼

查看編譯後的 java 代碼:

private final void callFunction() {
  int $i$f$inlined = false;
  Log.e("inline", "1");
  int var3 = false;
  Log.e("inline", "2");
  Log.e("inline", "3");
}
複製代碼

具體化的參數類型

inline 內聯函數還提供了另外一個有意思的能力:reified

reified 主要簡化了訪問類型參數的能力,看以下代碼:

private inline fun <T: Activity> inlined(clazz: Class<T>) {
    body()
    Log.e("inline", "${clazz.name}")
}

調用:
inlined(MainActivity::class.java)
複製代碼

其實沒什麼問你題,就是看起來不是很優雅(裝X),那怎麼辦呢。使用 reified 改造一下:

private inline fun <reified T: Activity> inlined() {
    body()
    Log.e("inline", "${T::class.java.name}")
}

調用:
inlined<MainActivity>()
複製代碼

查看編譯後的 java 代碼其實沒什麼差異,就是簡便輕巧!

總結

本節主要介紹了 kotlin 中的高階函數和內聯函數。高階函數能夠將函數用做參數或返回值,可是使用高階函數會有必定的性能損耗,可使用 inline 修飾爲內聯函數以免性能損耗,而且爲了不代碼量會增長,因此應避免內聯過大的函數。另外使用 noinlinecrossinline 修飾符能夠限制 lambda 傳參中直接使用 return 關鍵字以免影響函數正常執行。內聯函數還提供了 reified 簡化在函數中使用類型參數。

歡迎留言一塊兒交流學習!

相關文章
相關標籤/搜索