KOTLIN 高阶函数和内联函数
高阶函数
将函数作为参数或返回类型是函数的函数。
内联函数
背景
在 Kotlin 中,高阶函数或 lambda 表达式都存储为一个对象,因此对于函数对象和类,内存分配以及虚拟调用可能会引入运行时开销。有时,我们可以通过内联 lambda 表达式来消除内存开销。
方案
为了减少此类高阶函数或 lambda 表达式的内存开销,我们可以使用inline关键字,该关键字最终请求编译器不分配内存,而只需在调用位置复制该函数的内联代码即可。
分析
Kotlin代码会经过编译器转换为字节码,可以通过字节码查看区别:
public final class InlineTestKt {
public static final void higherFunc(@NotNull String str, @NotNull Function1 mycall) {
int $i$f$higherFunc = 0;
Intrinsics.checkNotNullParameter(str, "str");
Intrinsics.checkNotNullParameter(mycall, "mycall");
mycall.invoke(str);
}
public static final void main() {
higherFunc("A Computer Science portal for Geeks", (Function1)null.INSTANCE);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
Mycall通过将字符串作为参数,传递给println,调用println时它将创建一个额外的调用,并增加内存开销。其工作原理为:
mycall(new Function() {
@Override public void invoke() {
//println statement is called here.
}
});
如果我们调用大量的函数作为参数,则每个函数加起来都会增加方法计数,那么对内存和性能会有影响。如果为higherFunc加上inline关键字,反编译结果如下:
public final class InlineTestKt {
public static final void higherFunc(@NotNull String str, @NotNull Function1 mycall) {
int $i$f$higherFunc = 0;
Intrinsics.checkNotNullParameter(str, "str");
Intrinsics.checkNotNullParameter(mycall, "mycall");
mycall.invoke(str);
}
public static final void main() {
String str$iv = "A Computer Science portal for Geeks";
int $i$f$higherFunc = false;
int var3 = false;
System.out.println(str$iv);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
inline关键字,println lambda表达式以System.out.println
的形式被复制到main函数中,从而不需要调用一次。
结论
高阶函数需要传递一个函数或返回一个函数,这个函数的存在会增加一次额外调用,增加内存开销。如果我们存在一个高阶函数有大量的函数参数,每个函数的调用都会增加对内存和性能的压力。通过inline关键字,可以将函数调用转换为函数参数中的代码,从而减少中间层的调用带来的内存和性能开销。
crossinline 和 noinline
背景
在Kotlin中,如果我们想从lambda表达式中return,kotlin编译器会报错。例如:
var lambda = {
println("Lambda expression")
return // 编译期提示 'return' is not allowed here
}
注释掉return这一行,就可以正常运行。
问题
那么如何才能在lambda表达式中使用return呢?
将带有return的lambda表达式作为inline函数的参数
通过使用inline关键字,我们可以从lambda表达式中return,并退出调用inline函数的外部调用函数。
inline fun inlinedFunc1( lmbd1: () -> Unit, lmbd2: () -> Unit) {
lmbd1()
lmbd2()
}
fun main(){
println("Main function starts")
inlinedFunc1(
{
println("Lambda expression 1")
return
},
{
println("Lambda expression 2")
return
}
)
println("Main function ends")
}
输出结果为:
Main function starts
Lambda expression 1
当lmbd1执行到return时,main函数也直接return了。
crossinline
在上面的代码中,如果我们要阻止在lambda中使用return,可以通过使用crossinline关键字实现:
inline fun inlinedFunc2(crossinline lmbd1: () -> Unit, lmbd2: () -> Unit) {
lmbd1()
lmbd2()
}
fun main() {
inlinedFunc2(
{
println("Lambda expression 1")
return // 编译期提示 'return' is not allowed here
},
{
println("Lambda expression 2")
return
}
)
}
noinline
如果我们想指定内联函数的某个函数参数不进行内联操作,我们可以使用noinline修饰符进行标记。
在 Kotlin 中,如果我们只想将传递给内联函数的部分 lambda 进行内联,我们可以使用 noinline 修饰符标记一些函数参数。
inline fun inlinedFunc3(lmbd1: () -> Unit, noinline lmbd2: () -> Unit) {
lmbd1()
lmbd2()
}
main函数中调用的反编译结果:
public static final void main() {
String var0 = "Main function starts";
System.out.println(var0);
// 创建lmbd2函数对象
Function0 lmbd2$iv = (Function0)null.INSTANCE;
// lmbd1,内联后转换为调用内部代码System.out.println
int $i$f$inlinedFunc3 = false;
int var2 = false;
String var3 = "Lambda expression 1";
System.out.println(var3);
// lmbd2
lmbd2$iv.invoke();
var0 = "Main function ends";
System.out.println(var0);
}
从反编译结果可以看出,lmbd1,直接将内部代码在main函数中调用,这就是内联;而lmbd2则是创建了一个对象后调用invoke来实现代码运行的,noinline导致lmbd2没有内联。
reified 具体化的类型参数
在使用泛型的前提下,有时我们需要在调用时传递参数的类型。我们必须在函数调用时指定参数的类型,并通过 reified 修饰符来检索参数的类型。
inline fun <reified T> genericFunc() {
print(T::class)
}
fun main() {
genericFunc<String>()
}
打印结果为:class java.lang.String (Kotlin reflection is not available)
如果我们去掉reified关键字,此时print所在的一行报错:cannot use 'T' as reified type parameter. Use a class instead.
结论
在内联函数的场景下,有的时候我们需要传递参数类型的泛型,传递泛型需要使用reified关键字来实现。
内联属性
内联函数复制了代码到调用位置,同样地,inline关键字修饰的属性,实际就是将属性的getter方法实现代码复制到调用位置。inline修饰的属性不能有backing field。(如果定义为变量,setter内不能给field赋值,也就是说setter失效,或定义为常量。)
inline val flag : Boolean
get() = foo(10) == 10
fun foo(i: Int): Int {
return i
}
fun main(){
print(flag)
}
打印结果为true。如果flag的定义如下:
inline var flag : Boolean
get() = foo(10) == 10
set(value) {
value
}
在运行时,先将flag赋值为false,打印结果仍为true:
fun main() {
flag = false
print(flag)
}