[没那么难懂] Kotlin inline noinline crossinline详细并通俗版讲解,一篇就够
关于面试官喜欢问的inline noinline crossinline,我在这里写一下自己的理解。
🌟Inline关键字
inline关键字众所周知是标记某个函数为内联函数:
inline fun inlineFunc() {
println("inlineFunc")
}
说白了就是调用inlineFunc时相当于在调用处展开该函数:
fun main() {
// 调用inlineFunc()相当于直接把内容在这里展开:
println("inlineFunc")
}
上面这个看似很简单,但是知道这个远远不够,万一参数是lambda表达式呢?函数该怎么展开?
我们先写一个接受lambda表达式为参数的内联函数。
inline fun inlineFunc(a: () -> Unit) {
a()
println("inlineFunc")
}
如果调用者这样调用函数:
fun main() {
inlineFunc {
println("666")
}
}
就会出现歧义,编译器问:要展开成这样吗?⬇️
fun main() { // 第一种展开方式
println("666")
println("inlineFunc")
}
还是这样?⬇️
fun main() { // 第二种展开方式
// 生成一个函数,返回值为Unit,直接invoke执行
object : Function0<Unit> {
override fun invoke() {
println("666")
}
}.invoke()
println("inlineFunc")
}
你可能会疑惑,这两种方式有什么本质上的区别吗?
当然有!
第一种展开方式把 “lambda表达式的内容” 看成是一串代码了。
第二种方法把lambda表达式看成是一个匿名函数对象了,
先决知识:因为kotlin中实现lambda表达式是生成一个“函数对象”,那如果在循环中调用,就会频繁创建对象,非常占用性能,inline关键字初衷就是为了解决频繁创建函数对象而带来的开销。所以Kotlin规定“inline内联函数默认把所有lambda参数都到对应位置展开”,也就是第一种展开方式:
inline fun inlineFunc(a: () -> Unit) {
a()
println("inlineFunc")
}
fun main() {
inlineFunc {
println("666")
}
}
// ********************************相当于:
fun main() {
println("666")
println("inlineFunc")
}
思考一下,如果用return会是什么情况?inline最普通的这种情况下,如果用return,是可以return掉main函数的,因为“在调用处展开函数内容”,如果有return,就返回主函数了。当然,也可以return@inlineFunc,意思是返回这个lambda表达式,提前终止,但是inlineFunc函数里接下来的代码还是会执行的。
那么,怎么才能让编译器实现第二种展开方式呢?那就是接下来要介绍的noinline关键字了!
🌟Noinline关键字是给参数标的
和inline关键字不同之处在于,noinline是给lambda表达式的参数标记的。
刚刚说过:inline标记函数,编译器会默认把所有lambda参数都到对应位置展开,而有的lambda参数不想内联怎么办?
被noinline标记的参数会默认不内联,也就是说把完整的函数调用保留下来:
inline fun inlineFunc(a: () -> Unit, noinline b: () -> Unit) {
a()
b()
println("inlineFunc")
}
fun main() {
inlineFunc ({
println("我是a")
}, {
println("我是b")
})
}
// ********************************相当于:
fun main() {
println("我是a")
object : Function0<Unit> {
override fun invoke() {
println("我是b")
}
}.invoke()
println("inlineFunc")
}
说到底,noinline其实跟普通你写的fun myFunc(a: () -> Unit)一样,a这个参数都生成了一个匿名函数对象。
这样做有什么好处呢?noinline把一个lambda表达式当作了一个“函数对象”来看待,所以这个对象可以多次调用,就可以给别人传递。缺点却是不能return,只能return@inlineFunc。
而inline是把你写的lambda表达式当作了一句句代码来看(因为你要用inline展开函数来优化代码,不要每次都创建一个函数对象),当然不能传给其他函数执行。
inline fun inlineFunc(a: () -> Unit) {
a()
}
fun main() {
inlineFunc {
return@inlineFunc // Ok 任何情况下返回当前lambda表达式都没有问题,
// 因为你想提前中断lambda表达式的执行,很正常。
return // Ok 只有这个函数是inline函数时,才能直接return(因为inline是把函
// 数和lambda参数都在调用处展开,所以lambda参数有return,当然也要展
// 开出来),这里根据上下文,看到返回的是主函数。
}
}
// **但是如果inlineFunc把lambda表达式乱传:
inline fun inlineFunc(a: () -> Unit) {
postDelayed(a, 1000L) // 这种写法完全不可以,因为kotlin规定inline函数
// 的lambda参数 不是函数对象了,当然不可以乱传。
postDelayed({ a() }, 1000L) // 聪明的你想到方法总比困难多,用新的lambda表达
// 式包裹不就行了吗?很遗憾,这样写是有歧义的(接着看)。
}
看个栗子:刚刚说的inline的函数的lambda参数里是可以写return的,上面postDelayed({ a() }, 1000L),如果a里面有return,你肯定想要的是return掉调用这个inlineFunc的那个函数(否则你就写return@inlineFunc了),那1秒后handler的looper执行到return,但是main函数早就运行完毕了,或者还没运行完毕,你却要return它,就非常矛盾了(想一想,同一个线程和不同线程会怎么样?),而且如果kotlin语言实现是:把你的inlined lambda代码打包一份发到postDelayed,那打包的代码里如果有return,是不是就终止了handler里的runable的执行,也不是你想象的return掉调用这个inlineFunc的那个函数?
所以kotlin干脆禁止inline函数里{ a() }这种间接调用的写法防止你乱传,要写就只能直接调用a()。这样就没有歧义了。
postDelayed({ a() }, 1000L)
Error~~~~^^^~~~~~:Can’t inline ‘a’ here: it may contain non-local returns. Add ‘crossinline’ modifier to parameter declaration ‘a’
而noline这个关键字也没啥特殊的地方,就是说在本该都要内联的lambda表达式参数中,指定几个不内联的lambda表达式参数。所以:
inline fun myFunc(noinline a: () -> Unit)
等价于:
fun myFunc(a: () -> Unit)
这个时候a是函数对象,可以“间接调用”,不管你是写postDelayed(a, 1000L)也好,
postDelayed({ a() }, 1000L)也好,都可以,毕竟a里面不能直接return外层函数了,肯定没什么问题。
所以普通函数的lambda表达式参数,和标记了noinline的lambda表达式参数,都可以被间接调用。
而inline的lambda表达式参数,之所以不能间接调用,其实说了那么多,就是return这个作用域有迷惑性(不能理解可以再看看上面的栗子),那么禁用掉return,是不是就可以“既有inline的便利,又不会有return的迷惑性”了?这就是crossinline!
🌟Crossinline关键字也是给参数标的
inline fun myFunc(crossinline a: () -> Unit) { // 这时候a里面不能写return了!允许间接调用。
postDelayed(a, 1000L) // Error: a不是一个函数对象,它被内联了,只是一串代码
postDelayed({ a() }, 1000L) // Ok: 把这串代码打包发到postDelayed,为什么能打包呢,
// 因为crossinline不允许lambda参数里面写return
}
这样既消除了return作用域的迷惑性,又有inline的便利性(crossinline本质上还是展开代码,而不是生成匿名函数对象,减少开销)。
crossinline还有一个另类用法,先开栗子:
inline函数直接使用return,可能会破坏函数的原子性(就是myFunc函数可能没有执行完),例如:
inline fun myFunc(a: () -> Unit) {
a()
收尾工作
}
这样写看似没什么问题,万一调用者在a里写了直接return(他要真这么写,你也不能说什么hhhh:D ),那么“收尾工作”就不会执行,破坏函数原子性了,函数没有执行完毕,可能产生脏数据,这样就有可能导致奇奇怪怪的bug,这是一个inline的坑。可以考虑使用 crossinline 修饰符来标记 lambda 表达式参数,以禁止在 lambda 表达式中使用非局部返回(直接return),这样又不影响该参数inline。
🌟总结
- 普通函数的lambda表达式参数:
会生成函数对象,不适合在循环里调用,有开销。可以间接调用,不能非局部返回(直接return)。 - inline函数:
内联该函数(在对应位置展开),以及它的lambda参数。不可以间接调用,允许非局部返回。 - noinline修饰符:
标记lambda参数,让被标记的参数不被内联。可以间接调用,不能非局部返回(跟(1.)一样)。 - crossinline修饰符:
内联该参数,但由于return作用域有迷惑性,故直接禁止。可以间接调用,不能非局部返回。
观察发现,只有inline可以局部返回,不能间接调用。
此外,可以去学一下**“泛型实化”reified关键字**,可以配合inline使用,函数调用时确定泛型的类型,岂不美哉?
如果有帮助记得点个赞收藏或者点个关注,谢谢:)🙏