Kotlin-内联函数

1,298 阅读5分钟

本篇是记录学习《Kotlin实战》一书中关于内联的概念和知识点

1.内联函数介绍

    在Kotlin中如果使用inline修饰符标记的一个函数,被称为内联函数。在解释内联函数的作用之前,我们先来看下如果没有内联修饰符标记函数,在使用lambda带来的性能开销。举个接受函数类型的例子

//callAction 接受一个函数类型(lambda)
private fun callAction(action: () -> Unit) {
    println("call Action before")
    action()
    println("call Action after")
}

fun main(args: Array<String>) {
    callAction {
        println("call action")
    }
}

这两个函数会被编译为

public final void main(@NotNull String[] args) {
      callAction((Function0)null.INSTANCE);
}

 private final void callAction(Function0 action) {
      String var2 = "call Action before";
      boolean var3 = false;
      System.out.println(var2);
      action.invoke();
      var2 = "call Action after";
      var3 = false;
      System.out.println(var2);
   }

    由此可见当调用callAction(action: () -> Unit) 时,传递的lambda会被Function0所代替,而Function0是一个被定义为如下的接口

public interface Function0<out R> : Function<R> {
    public operator fun invoke(): R
}

也就是说在调用callAction时,编译器会额外生成一个Function0的实例传递给callAction,内部会调用 Function0invoke() 方法。到目前为止,我们知道使用lambda会带来额外的性能开销。那有什么办法可以解决此问题呢?答案当然有,通过 内联函数消除lambda带来的运行时开销

2 内联函数的作用

    在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用。还是拿 callAction(action: () -> Unit) 方法举例,当给该函数添加inline修饰符后,编译后的调用代码如下

public final void main(@NotNull String[] {
      ...省略无关紧要的代码
      System.out.println("call Action before");
      System.out.println("call action");
      System.out.println("call Action after");
}

代码结构如此清晰。 总结下:

  1. 被inline修饰的函数叫内联函数
  2. 内联函数会在被调用的位置内联。内联函数的代码会被拷贝到使用它的位置,并把lambda替换到其中

3 内联函数的限制

    鉴于内联的运作方式,不是所有使用lambda的函数都可以被内联。当函数被内联的时候,作为参数的lambda表达式的函数体会被直接替换到最终生成的代码中。这将限制函数体中的对应(lambda)参数的使用。如果(lambda)参数被调用,这样的代码能被容易地内联。但如果(lambda)参数在某个地方被保存起来,以便后面可以继续使用,lambda表达式的代码将不能被内联,因为必须要有一个包含这些代码的对象存在。

    例如下面的例子,action被隐式保存在runAction函数中,然后再传递到callAction中。此时lambda表达式代码将不能被内联。

fun main(args: Array<String>) {
        runAction {
            println("run Action")
        }
}
private fun runAction(action: () -> Unit) {
        println("do something")
        callAction(action)
        println("do something")
}
private inline fun callAction(action: () -> Unit) {
        println("call Action before")
        action()
        println("call Action after")
}

    编译后的代码如下略有删减只保留重要部分

public final void main(@NotNull String[] args) {
      runAction((Function0)null.INSTANCE);
 }

private final void runAction(Function0 action) {
      System.out.println("do something");
      System.out.println("call Action before");
      action.invoke();
      System.out.println("call Action after");
      System.out.println("do something");
   }

    如果想让callAction内联只能将runAction也改为内联函数。一般来说,参数如果被直接调用或者作为参数传递给另外一个inline函数,它是可以被内联的。(内联函数传递内联函数)

    如果一个函数期望两个或更多lambda参数,可以选择只内联其中一些参数。这是有道理的,因为一个lambda可能会包含很多代码或者以不允许内联方式使用。接口这样的非内联lambda的参数,可以用noinline修饰符来标记它。

private inline fun callAction(action: () -> Unit, noinline doSomething: () -> Unit) {
}

5 内联函数另一妙用

    无论使用Java还是Kotlin泛型时,在运行时都无法避免泛型擦除。例如如下例。

fun main(args: Array<String>) {
    printSum(listOf<String>("a", "b", "c"))
}

fun printSum(c: Collection<*>) {
    val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected")
    println(intList.sum())
}

    在调用printSum()时传递的是List泛型为String类型 运行时由于泛型擦除那么List泛型String类型和List 泛型Int类型变为List。那么第一行实际是 List as? List 成立。在调用sum方法时由于实际参数是String,运行时会得到一个ClassCastException。

    Kotlin泛型在运行时会被擦除,这意味着如果你有一个泛型类的实例,你无法弄清楚在这个实例创建时用的究竟是哪些类型实参。泛型函数的类型实参也是这样。在调用泛型函数的时候,在函数体中你不能决定调用它用的类型实参。例如

    fun <T> isA(value :Any) =value is T

    这行代码编译器会报错:==Cannot check for instance of erased type:T== (无法检查已擦除类型的实例:T )

    通常情况下都是这样,只有一种例外可以避免这种限制:内联函数。内联的类型形参能够被实化,意味着你可以在运行时引用实际的类型实参。

inline fun <reified T> isA(value :Any) =value is T

    接下来看看使用实化类型参数来代替传递作为java.lang.Class的activity类的妙用。

//类型参数标记为"reified"
inline fun <reified T : Activity> Context.startActivity() {
    //把T:class当做类型参数的类访问
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}
<!--MainActivity调用-->
startActivity<MainActivity>()

6 决定何时将函数声明成内联

    在使用inline关键字的时候,你还是应该注意代码长度。如果你要内联的函数很大,将它的字节码拷贝到每个调用点将会极大地增加字节码的长度。在这种情况下,你应该将那些与lambda参数无关的代码抽取到一个独立的非内联函数中,或者将不依赖实化类型参数的代码抽取到单独的非内联函数中,你可以自己去验证下,在Kotlin标准库中的内联函数总是很小。