vlambda博客
学习文章列表

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: () -> Unitnoinline 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)
}