kotlin中lambda的实现和内联函数

前言

通过阅读lambda表达式与Kotlin高阶函数,你应该了解到在kotlin中传递lambda作为函数参数的语法与普通的表达式很相似。这篇文章则带你了解lambda的运作原理以及用来消除lambda带来的运行时开销的内联函数。

通过字节码分析lambda表达式

我们先申明一个高阶函数lambdaFunction,并使用lambda作为实参对齐进行低啊用:

object Lombda {
    @JvmStatic
    fun main(arg: Array) {
        val lambdaFunction = lambdaFunction({ x, y -> x + y })
        println(lambdaFunction)
    }

    private fun lambdaFunction(function: (Int, Int) -> Int): Int {
        return function(2, 3)
    }
}

在Intellij IDEA中将其字节码文件反编译为java文件,代码如下:

public final class Lombda {
   public static final Lombda INSTANCE;

   @JvmStatic
   public static final void main(@NotNull String[] arg) {
      Intrinsics.checkParameterIsNotNull(arg, "arg");
      int lambdaFunction = INSTANCE.lambdaFunction((Function2)null.INSTANCE);
      System.out.println(lambdaFunction);
   }

   private final int lambdaFunction(Function2 function) {
      return ((Number)function.invoke(Integer.valueOf(2), Integer.valueOf(3))).intValue();
   }

   private Lombda() {
      INSTANCE = (Lombda)this;
   }

   static {
      new Lombda();
   }
}

点击Function2进入Functions.kt文件,部分代码截图如下:

在kotlin.jvm.functions包下Functions.kt文件中定义了一系列的接口,这些接口对应于不同参数数量的函数(in为函数的入参,out为函数的返回值)。每个接口定义了一个invoke方法没调用这个方法就会执行函数。一个函数类型的变量就是实现了对应FunctionN接口的实现类的实例,实现类的invoke方法包含了lambda函数体。一个lambda表达式就是一个FunctionN接口的实现。每个lambda表达式都会被编译一个匿名类(除非它是一个内联函数)。一旦实现,编译器就可以避免为每一个表达式都生成一个class文件。如果lambda表达式捕捉了变量,每个被捕捉的变量会在匿名类中有对应的字段。而且每次对lambda表达式的调用都会创建一个该匿名类新的实例。
了解了lambda函数的实现细节,可以发现:每次调用一次lambda都会额外创建一个类,而且如果lambda捕捉了变量,每次调用都会创建一个新的对象。这回带来运行时的额外开销。接下来就由内联函数来解决这个问题

内联函数:消除lambda带来的运行时开销

如果使用inline修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现真实的代码替换每一次的函数调用。

也就是说:函数体会被直接替换到函数被调用的地方,而不是正常的被调用

内联函数运作方法

首先我们定义一个内联函数,并对它进行调用

inline fun lambdaFunction(action:(Int,Int)->Unit){
        action(2,3)
 }   
 
fun main() {
        println("调用前")
        lambdaFunction { x, y -> print(x+y) }
        println("调用后")
    }

下面这段代码和main()的作用相同,并且会被编译为同样的字节码

fun _main_(){
        println("调用前")
        print(2+3)
        println("调用前")
    }

其中lambda表达式被内联了。由lambda表达式生成的自己码成为了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的匿名类中。(我个人的理解为:抽取代码块,编译时将代码块填充进指定位置,即调用内联函数的位置)。函数体会被直接替换到函数被调用的地方。
如果在两个不同的位置使用同一个内联函数,并且使用的lambda不同,那么内联函数会在每一个被调用的位置被分别内联。内联函数的代码会被拷贝到使用它的位置,并把不同的lambda替换到其中。

内联函数的限制

并不是所有的lambda函数都可以被内联。
如果lambda在某个地方被保存起来,lambda表达式的代码将不能被内联。
例如:

val la = {x:Int, y:Int -> print(x+y)}
lambdaFunction (la)

以函数类型的变量作为参数调用内联函数,这种情况下lambda并不会被内联。
需要注意的是:使用内联函数只能提高带有lambda参数的函数的性能。对于普通函数而言,JVM已经提供了强大的内联支持,在字节码中,每个函数的实现只会出现一次,并不需要跟内联函数一样,每个调用的地方都拷贝一次。而且,如果函数直接被调用,调用栈会更加清晰易读。
使用内联函数能给我们带来明显的好处:
避免运行时开销(节约函数调用,lambda创建匿名类和实例对象的开销)

结语

至此,kotlin中的lambda和内联函数已经基本分析完毕。kotlin标准库中提供了许多内联函数,以及操作集合的函数。这些函数可以大大减少代码量和工作效率。持续关注我的博客,了解更多kotlin标准库函数。

你可能感兴趣的:(kotlin中lambda的实现和内联函数)