Kotlin inline noinline crossinline 解答

Kotlin 針對函數提供了幾個關鍵字 inline noinline crossinline,其涉及 Kotlin 中內聯函數和 lambda相關的問題。java

概覽

  • inline: 聲明在編譯時,將函數的代碼拷貝到調用的地方(內聯)
  • oninline: 聲明 inline 函數的形參中,不但願內聯的 lambda
  • crossinline: 代表 inline 函數的形參中的 lambda 不能有 return

inline

使用 inline 聲明的函數,在編譯時將會拷貝到調用的地方。閉包

inline function

定義一個sum函數計算兩個數的和。jvm

fun main(args: Array<String>) {
    println(sum(1, 2))
}

fun sum(a: Int, b: Int): Int {
    return a + b
}
複製代碼

反編譯爲 Java 代碼:函數

public static final void main(@NotNull String[] args) {
   int var1 = sum(1, 2);
   System.out.println(var1);
}

public static final int sum(int a, int b) {
   return a + b;
}
複製代碼

正常的樣子,在該調用的地方調用函數。工具


而後爲 sum 函數添加 inline 聲明:性能

inline fun sum(a: Int, b: Int): Int {
    return a + b
}
複製代碼

再反編譯爲 Java 代碼:優化

public static final void main(@NotNull String[] args) {
   //...
   byte a$iv = 1;
   int b$iv = 2;
   int var4 = a$iv + b$iv;
   System.out.println(var4);
}

public static final int sum(int a, int b) {
   return a + b;
}
複製代碼

sum 函數的實現代碼被直接拷貝到了調用的地方。ui


上面兩個使用實例並無體現出 inline 的優點。當你的函數中有 lambda 形參時,inline 的優點纔會體現。spa

inline function with lambda parameters

考慮以下代碼,會被編譯成怎樣的 Java 代碼?code

fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit): Int {
    val r = a + b
    lambda.invoke(r)
    return r
}

fun main(args: Array<String>) {
    sum(1, 2) { println("Result is: $it") }
}
複製代碼

反編譯爲 Java:

public static final int sum(int a, int b, @NotNull Function1 lambda) {
   //...
   int r = a + b;
   lambda.invoke(r);
   return r;
}

public static final void main(@NotNull String[] args) {
   //...
   sum(1, 2, (Function1)null.INSTANCE);
}
複製代碼

(Function1)null.INSTANCE,是因爲反編譯器工具在找不到等效的 Java 類時的顯示的結果。

我傳遞的那個 lambda 被轉換爲 Function1 類型,它是 Kotlin 函數(kotlin.jvm.functions包)的一部分,它以 1 結尾是由於咱們在 lambda 函數中傳遞了一個參數(result:Int)。

再考慮以下代碼:

fun main(args: Array<String>) {
    for (i in 0..10) {
        sum(1, 2) { println("Result is: $it") }
    }
}
複製代碼

我在循環中調用 sum 函數,每次傳遞一個 lambda 打印結果。反編譯爲 Java:

for(byte var2 = 10; var1 <= var2; ++var1) {
    sum(1, 2, (Function1)null.INSTANCE);
}
複製代碼

可見在每次循環裏都會建立一個 Function1 的實例對象。這裏就是性能的優化點所在,如何避免在循環裏建立新的對象?


  1. 在循環外部建立 lambda 對象
val l: (r: Int) -> Unit = { println(it) }

for (i in 0..10) {
    sum(1, 2, l)
}
複製代碼

反編譯爲 Java:

Function1 l = (Function1)null.INSTANCE;
int var2 = 0;

for(byte var3 = 10; var2 <= var3; ++var2) {
    sum(1, 2, l);
}
複製代碼

只會建立一個 Function 對象

  1. 使用 inline:
fun main(args: Array<String>) {
    for (i in 0..10) {
        sum(1, 2) { println("Result is: $it") }
    }
}

inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit): Int {
    val r = a + b
    lambda.invoke(r)
    return r
}
複製代碼

反編譯爲 Java:

public static final void main(@NotNull String[] args) {
   //...
   int var1 = 0;

  for(byte var2 = 10; var1 <= var2; ++var1) {
     byte a$iv = 1;
     int b$iv = 2;
     int r$iv = a$iv + b$iv;
     String var9 = "Result is: " + r$iv;
     System.out.println(var9);
  }
}
複製代碼

lambda 代碼在編譯時被拷貝到調用的地方, 避免了建立 Function 對象。

inline 注意事項

public inline 函數不能訪問私有屬性

class Demo(private val title: String) {

    inline fun test(l: () -> Unit) {
        println("Title: $title") // 編譯錯誤: Public-Api inline function cannot access non-Public-Api prive final val title
    }

    // 私有的沒問題
    private inline fun test(l: () -> Unit) {
        println("Title: $title")
    }
}
複製代碼

注意程序控制流

當使用 inline 時,若是傳遞給 inline 函數的 lambda,有 return 語句,那麼會致使閉包的調用者也返回。

例子:

inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit): Int {
    val r = a + b
    lambda.invoke(r)
    return r
}

fun main(args: Array<String>) {
    println("Start")
    sum(1, 2) {
        println("Result is: $it")
        return // 這個會致使 main 函數 return
    }
    println("Done")
}
複製代碼

反編譯 Java:

public static final void main(@NotNull String[] args) {
   String var1 = "Start";
   System.out.println(var1);
   byte a$iv = 1;
   int b$iv = 2;
   int r$iv = a$iv + b$iv;
   String var7 = "Result is: " + r$iv;
   System.out.println(var7);
}
複製代碼

反編譯以後也能看到,lambda return 以後的代碼不會執行。

如何避免?

可使用 return@label 語法,返回到 lambda 被調用的地方。

fun main(args: Array<String>) {
    println("Start")
    sum(1, 2) {
        println("Result is: $it")
        return@sum
    }
    println("Done")
}
複製代碼

noinline

當一個 inline 函數中,有多個 lambda 做爲參數時,能夠在不想內聯的 lambda 前使用 noinline 聲明.

inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit, noinline lambda2: (result: Int) -> Unit): Int {
    val r = a + b
    lambda.invoke(r)
    lambda2.invoke(r)
    return r
}

fun main(args: Array<String>) {
    sum(1, 2,
            { println("Result is: $it") },
            { println("Invoke lambda2: $it") }
    )
}
複製代碼

反編譯 Java:

public static final int sum(int a, int b, @NotNull Function1 lambda, @NotNull Function1 lambda2) {
   int r = a + b;
   lambda.invoke(r);
   lambda2.invoke(r);
   return r;
}

public static final void main(@NotNull String[] args) {
   byte a$iv = 1;
   byte b$iv = 2;
   Function1 lambda2$iv = (Function1)null.INSTANCE;
   int r$iv = a$iv + b$iv;
   String var8 = "Result is: " + r$iv;
   System.out.println(var8);
   lambda2$iv.invoke(r$iv);
}
複製代碼

第一個 lambda 內聯到了調用處,而第二個使用 noinline 聲明的 lambda 沒有。

crossinline

聲明一個 lambda 不能有 return 語句(能夠有 return@label 語句)。這樣能夠避免使用 inline 時,lambda 中的 return 影響程序流程。

inline fun sum(a: Int, b: Int, crossinline lambda: (result: Int) -> Unit): Int {
    val r = a + b
    lambda.invoke(r)
    return r
}

fun main(args: Array<String>) {
    sum(1, 2) {
        println("Result is: $it")
        return  // 編譯錯誤: return is not allowed here
    }
}
複製代碼

總結

  • 使用 inline,內聯函數到調用的地方,能減小函數調用形成的額外開銷,在循環中尤爲有效
  • 使用 inline 能避免函數的 lambda 形參額外建立 Function 對象
  • 使用 noinline 能夠拒絕形參 lambda 內聯
  • 使用 crossinline 顯示聲明 inline 函數的形參 lambda 不能有 return 語句,避免lambda 中的 return 影響外部程序流程
相關文章
相關標籤/搜索