Kotlin的inline、noinline、crossinline全面分析2

前言

前面我们说了inline关键字,不多说它很好用,

# Kotlin的inline、noinline、crossinline全面分析1

它还有2个好兄弟noinline和crossinline,我们继续来分析。

正文

noinline作为inline的反义,字面意思就是不内联,那和普通函数不就一样了? 当然不是,inline是用来修饰函数的,而这个noinline只能修饰函数的参数,也就是函数是内联的,但是这个函数类型参数不是内联的。

既然知道了这个定义,我们来看看它有什么用。

noinline

话不多说,直接看个例子:

//函数是内联的,但是参数action不是内联的
inline fun lambdaFun(noinline action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    action()
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

然后我们调用:

//调用
fun testHello(){
    lambdaFun {
        Log.i("zyh", "testLambdaFun: 调用中")
    }
}
复制代码

直接反编译:

//反编译代码
public final class TestFun {
   public final void testHello() {
       //创建了匿名内部类实例
      Function0 action$iv = (Function0)null.INSTANCE;
      int $i$f$lambdaFun = false;
      Log.i("zyh", "testLambdaFun: 调用前");
      action$iv.invoke();
      Log.i("zyh", "testLambdaFun: 调用后");
   }
}
复制代码

这里我们清晰的看出lambdaFun内部的代码进行了复制铺平到调用地方,但是对于action却没有复制铺平,原因也非常简单,因为它是noinline修饰的,那它有什么用呢 我们接着分析。

使用noinline的原因

我们前面说了inline会让函数类型参数进行复制和铺平,那这个参数也就不再是函数类型参数了,毕竟它变成了几行代码,所以这就是局限性,当我们还要把它作为函数类型参数或者返回时,就要使用noinline了。

还是直接看例子:

//定义高阶函数,非内联
fun lambdaFun1(action: () -> Unit){
    action()
}
复制代码

然后我们定义一个内联函数:

//内联函数
inline fun lambdaFun(action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    action()
    //调用高阶函数
    lambdaFun1(action)
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

这种使用很常见,但是我们会发现这段代码无法编译:

image.png

原因也非常简单,想把action传递给lambdaFun1,那这个action必须函数类型参数,但是在被inline修饰的函数,其参数也会被铺平,也就不会再是函数类型了,所以这里的action要使用noinline来修饰:

//使用noinline修饰参数
inline fun lambdaFun(noinline action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    action()
    lambdaFun1(action)
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

高阶函数除了把函数类型参数当做其他高阶函数的参数外,还可以作为返回值,同样这时也不能把函数类型参数给铺平为lambda表达式,我们看例子:

image.png

这里我想返回这个action,遗憾的是,和前面一样,这个action会被铺平,将不再是函数类型参数了,所以必须把action用noinline修饰:

//因为返回值要使用action,要保留action为函数类型
inline fun lambdaFun(noinline action: (() -> Unit)):() -> Unit{
    Log.i("zyh", "testLambdaFun: 调用前")
    action()
    Log.i("zyh", "testLambdaFun: 调用后")
    return action
}
复制代码

上面的代码进行反编译,也能想象出,肯定函数内部会被内联,action会被编译成匿名内部类:

public final void testHello() {
   Function0 action$iv = (Function0)null.INSTANCE;
   int $i$f$lambdaFun = false;
   Log.i("zyh", "testLambdaFun: 调用前");
   //这里action不会被内联
   action$iv.invoke();
   Log.i("zyh", "testLambdaFun: 调用后");
}
复制代码

return难题

inline和noinline说完,我们来说一个return问题,为啥return会有不一样呢 我们来慢慢细说。

非内联高阶函数

先定义一个高阶函数:

fun lambdaFun(action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    action()
    Log.i("zyh", "testLambdaFun: 调用后")

}
复制代码

然后进行调用,在lambda中想使用return语句:

image.png

这里直接使用return语句无法使用,提醒使用return@lambdaFun,这当然可以理解,那我只想使用return呢 有没有办法,当然可以,把lambdaFun定义为内联函数。

内联函数

改成内联函数:

inline fun lambdaFun(action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    action()
    Log.i("zyh", "testLambdaFun: 调用后")

}
复制代码

然后再进行调用return语句:

image.png

这里居然不报错了,由于内联函数会把函数体和lambda给复制铺平到调用地方,所以这里的return必然是返回testHello函数了,而不是lambdaFun函数。

return和inline之间的约定

所以这里为了解决lambda表达式中的return语句问题,Kotlin直接规定,在非inline函数中,return无法使用(必须return@xxx指明返回的函数),只有在inline函数中可以使用return语句,这样就不会有异议,根据inline的特性,这个return必然是返回调用者函数。

crossinline

既然我们了解了return问题,以及它return和inline之间的约定,也就是为了不产生歧义,那我们继续看问题,引出crossinline这个修饰词了。

直接看例子,再定义一个高阶函数:

//新的函数,参数是函数类型
fun lambdaFun1(postAction: () -> Unit){
    postAction()
}
复制代码

然后在前面定义的函数中,调用这个函数:

//调用lambdaFun1,并且使用lambda
inline fun lambdaFun(action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    lambdaFun1 {
        action()
    }
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

千万注意,这里和前面说noinline的例子是不一样的,这里是使用lambda表达式,在表达式内调用action()也就是其invoke函数,而不是把action这个函数类型参数进行传递,和下面是不一样的:

inline fun lambdaFun(action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    //这里期望把action还是当做类型参数,而不是铺平
    lambdaFun1(action)
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

上面要区分开,现在我们是讨论第一种情况,这个代码能编译吗,我们看一下:

image.png

注意这里的报错信息:无法内联action参数,原因是它可能包含return语句。

哦?为什么呢?原因非常简单,这里的lambdaFun是内联函数,但是lambdaFun1是非内联的,根据前面return和内联函数之间的约定,只有内联函数可以使用return,否则不行,那这就尴尬了,不可能我每个内联函数都必须调用内联函数吧,这显然不符合逻辑。

突破限制,加强inline功能

因为上面的代码,永远不知道action会不会包含return语句,所以inline受限严重,这里只有突破这个限制才可以,这里采用的方式是把action加个crossinline修饰符。

然后会发现:

image.png

这里居然不报错了,看似问题解决了,它内部又做了什么优化吗 这种又内联又不内联的,怕是要把编译器整疯了,当然不是,它只是解决提出问题的人。

crossinline的作用

crossinline的作用仅仅是当有被这个修饰的参数会告诉IDE来检查你写的代码中有没有包含return,假如有的话会编译不过,就是这么简单暴力。

直接看例子对比一下,首先这里不加crossinline:

inline fun lambdaFun(action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    lambdaFun1 {
        action()
    }
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

调用的时候:

image.png

可以正常写return语句,当我们对这个参数加了crossinlie时:

inline fun lambdaFun(crossinline action: (() -> Unit)){
    Log.i("zyh", "testLambdaFun: 调用前")
    lambdaFun1 {
        action()
    }
    Log.i("zyh", "testLambdaFun: 调用后")
}
复制代码

再进行调用:

image.png

直接就不让写了,哈哈,真的简单暴力。

小结一下

其实前面我们已经说完了crossinline出现的原因以及解决的问题,以及最直接暴力解决问题的办法,但是还是总结一下,这里什么情况会出现这种问题。

也就是当在内联函数中,想把参数的执行放入其他函数中,这时这个其他函数和最外层调用这个内联函数的函数就分割开了,这时你就要注意了,这个参数是不是要用crossinline修饰了。

当然这个我们不必刻意,当你代码逻辑有问题时,这个修饰符IDE会自动提醒。

总结

最后还是总结一下子,这3个关键字涉及的东西还真不少。

  • 为了解决每次调用高阶函数时给它传递lambda时都会创建匿名内部类的问题引入了inline。

  • inline修饰的函数,不仅会把函数体的内容进行复制铺平,还会把函数类型参数的内容复制铺平。

  • 当在内联函数中想把某个函数类型的参数进行传递或者返回,这时就不能把这个参数给铺平,所以使用noinline修饰这个参数。

  • 在lambda中使用return语句会造成歧义,不知道返回哪一层,所以Kotlin直接定义非inline函数时不能使用return,当是inline函数时,return是返回调用着那一层函数。

  • 在内联函数中,调用非内联的高阶函数,把lambda传递到非内联函数中,将和上面造成冲突,无法判断这个参数的lambda中有没有return,为了解决这个问题,引入了crossinline。

  • crossinline的目的是告诉IDE,这个参数的lambda不能写return语句。

猜你喜欢

转载自juejin.im/post/7050729336080433188
今日推荐