Kotlin 針對函式提供了幾個關鍵字 inline noinline crossinline,其涉及 Kotlin 中行內函數和 lambda
相關的問題。
概覽
inline
: 宣告在編譯時,將函式的程式碼拷貝到呼叫的地方(內聯)oninline
: 宣告inline
函式的形參中,不希望內聯的lambda
crossinline
: 表明inline
函式的形參中的lambda
不能有return
inline
使用 inline
宣告的函式,在編譯時將會拷貝到呼叫的地方。
inline function
定義一個sum
函式計算兩個數的和。
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
函式的實現程式碼被直接拷貝到了呼叫的地方。
上面兩個使用例項並沒有體現出 inline
的優勢。當你的函式中有 lambda
形參時,inline
的優勢才會體現。
inline function with lambda parameters
考慮如下程式碼,會被編譯成怎樣的 Java 程式碼?
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
的例項物件。這裡就是效能的優化點所在,如何避免在迴圈裡建立新的物件?
- 在迴圈外部建立
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
物件
- 使用
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
影響外部程式流程