使用inline 防止对象创建
Lambda 被转换为类
在 Kotlin/JVM 中,函数类型 (lambda) 被转换为扩展接口 Function 的匿名/常规类。考虑以下函数:
fun doSomethingElse(lambda: () -> Unit) {
println("Doing something else")
lambda()
}
上面的函数,编译后如下所示:
public static final void doSomethingElse(Function0 lambda) {
System.out.println("Doing something else");
lambda.invoke();
}
函数类型() -> Unit转换为接口Function0。
现在让我们看看当我们从其他函数调用这个函数时会发生什么:
fun doSomething() {
println("Before lambda")
doSomethingElse {
println("Inside lambda")
}
println("After lambda")
}
问题:对象
编译器将 lambda 替换为 Function 类型的匿名对象:
public static final void doSomething() {
System.out.println("Before lambda");
doSomethingElse(new Function() {
public final void invoke() {
System.out.println("Inside lambda");
}
});
System.out.println("After lambda");
}
这里的问题是,如果你在循环中调用这个函数数千次,就会创建数千个对象并进行垃圾回收。这会影响性能。
解决方案:inline
通过在函数前添加 inline 关键字,我们可以告诉编译器在调用点复制该函数的代码,无需创建对象:
inline fun doSomethingElse(lambda: () -> Unit) {
println("Doing something else")
lambda()
}
这会导致inline 函数的代码以及lambda() 的代码在调用点被复制:
public static final void doSomething() {
System.out.println("Before lambda");
System.out.println("Doing something else");
System.out.println("Inside lambda");
System.out.println("After lambda");
}
如果您将有/没有inline 关键字与for 循环中的一百万次重复进行比较,这会使执行速度加倍。因此,将其他函数作为参数的函数在内联时会更快。
使用inline 防止变量捕获
当你在 lambda 中使用局部变量时,它被称为变量捕获(闭包):
fun doSomething() {
val greetings = "Hello" // Local variable
doSomethingElse {
println("$greetings from lambda") // Variable capture
}
}
如果我们这里的doSomethingElse() 函数不是inline,则在创建我们之前看到的匿名对象时,捕获的变量会通过构造函数传递给 lambda:
public static final void doSomething() {
String greetings = "Hello";
doSomethingElse(new Function(greetings) {
public final void invoke() {
System.out.println(this.$greetings + " from lambda");
}
});
}
如果您在 lambda 中使用了许多局部变量或在循环中调用 lambda,则通过构造函数传递每个局部变量会导致额外的内存开销。在这种情况下使用inline 函数有很大帮助,因为该变量直接在调用站点使用。
因此,从上面的两个示例中可以看出,inline 函数的大部分性能优势是在函数将其他函数作为参数时实现的。这是inline 函数最有用且最值得使用的时候。无需 inline 其他通用函数,因为 JIT 编译器已经在必要时将它们内联。
使用inline 获得更好的控制流程
由于非内联函数类型被转换为类,我们不能在lambda里面写return语句:
fun doSomething() {
doSomethingElse {
return // Error: return is not allowed here
}
}
这被称为非本地return,因为它不是调用函数doSomething() 的本地。不允许非本地return 的原因是return 语句存在于另一个类中(在前面显示的匿名类中)。制作doSomethingElse() 函数inline 解决了这个问题,我们可以使用非本地返回,因为这样return 语句就会被复制到调用函数中。
将inline 用于reified 类型参数
在 Kotlin 中使用泛型时,我们可以使用 T 类型的值。但是我们不能直接使用类型,我们得到错误Cannot use 'T' as reified type parameter. Use a class instead:
fun <T> doSomething(someValue: T) {
println("Doing something with value: $someValue") // OK
println("Doing something with type: ${T::class.simpleName}") // Error
}
这是因为我们传递给函数的类型参数在运行时被删除了。所以,我们不可能确切知道我们正在处理的是哪种类型。
使用inline 函数和reified 类型参数可以解决这个问题:
inline fun <reified T> doSomething(someValue: T) {
println("Doing something with value: $someValue") // OK
println("Doing something with type: ${T::class.simpleName}") // OK
}
内联导致实际类型参数被复制以代替T。因此,例如,当您调用像 doSomething("Some String") 这样的函数时,T::class.simpleName 变为 String::class.simpleName。 reified 关键字只能与 inline 函数一起使用。
在重复调用时避免使用inline
假设我们有以下在不同抽象级别重复调用的函数:
inline fun doSomething() {
println("Doing something")
}
第一抽象层
inline fun doSomethingAgain() {
doSomething()
doSomething()
}
结果:
public static final void doSomethingAgain() {
System.out.println("Doing something");
System.out.println("Doing something");
}
在第一个抽象级别,代码增长为:21 = 2 行。
第二抽象层
inline fun doSomethingAgainAndAgain() {
doSomethingAgain()
doSomethingAgain()
}
结果:
public static final void doSomethingAgainAndAgain() {
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
}
在第二个抽象级别,代码增长为:22 = 4 行。
第三抽象层
inline fun doSomethingAgainAndAgainAndAgain() {
doSomethingAgainAndAgain()
doSomethingAgainAndAgain()
}
结果:
public static final void doSomethingAgainAndAgainAndAgain() {
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
}
在第三个抽象级别,代码增长为:23 = 8 行。
类似地,在第四个抽象层,代码增长为 24 = 16 行,依此类推。
数字 2 是在每个抽象级别调用函数的次数。正如您所看到的,代码不仅在最后一级呈指数增长,而且在每一级都呈指数增长,因此是 16 + 8 + 4 + 2 行。我在这里只展示了 2 个调用和 3 个抽象级别以保持简洁,但想象一下会为更多调用和更多抽象级别生成多少代码。这会增加您的应用程序的大小。这也是为什么您不应该 inline 应用程序中的每个功能的另一个原因。
在递归循环中避免inline
避免将inline 函数用于函数调用的递归循环,如以下代码所示:
// Don't use inline for such recursive cycles
inline fun doFirstThing() { doSecondThing() }
inline fun doSecondThing() { doThirdThing() }
inline fun doThirdThing() { doFirstThing() }
这将导致函数复制代码的循环永无止境。编译器给你一个错误:The 'yourFunction()' invocation is a part of inline cycle。
隐藏实现时不能使用inline
公共的inline函数不能访问private函数,所以不能用于实现隐藏:
inline fun doSomething() {
doItPrivately() // Error
}
private fun doItPrivately() { }
在上面显示的inline 函数中,访问private 函数doItPrivately() 会出现错误:Public-API inline function cannot access non-public API fun。
检查生成的代码
现在,关于你问题的第二部分:
但是我发现kotlin没有为a创建函数对象
非内联函数。为什么?
Function 对象确实已创建。要查看创建的Function 对象,您需要在main() 函数中实际调用lock() 函数,如下所示:
fun main() {
lock { println("Inside the block()") }
}
生成的类
生成的Function 类不会反映在反编译的Java 代码中。您需要直接查看字节码。查找以:
开头的行
final class your/package/YourFilenameKt$main$1 extends Lambda implements Function0 { }
这是编译器为传递给lock() 函数的函数类型生成的类。 main$1 是为 block() 函数创建的类的名称。有时该类是匿名的,如第一节中的示例所示。
生成的对象
在字节码中,查找以:
开头的行
GETSTATIC your/package/YourFilenameKt$main$1.INSTANCE
INSTANCE 是为上述类创建的对象。创建的对象是一个单例,因此命名为INSTANCE。
就是这样!希望对 inline 函数提供有用的见解。