[没那么难懂] Kotlin inline noinline crossinline详细并通俗版讲解,一篇就够

本文详细解释了Kotlin中的inline、noinline和crossinline关键字,包括它们如何影响函数内联、lambda参数处理、性能优化以及非局部返回的限制。作者还提到了reified关键字的应用。
摘要由CSDN通过智能技术生成

[没那么难懂] 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。

🌟总结

  1. 普通函数的lambda表达式参数
    会生成函数对象,不适合在循环里调用,有开销。可以间接调用,不能非局部返回(直接return)。
  2. inline函数
    内联该函数(在对应位置展开),以及它的lambda参数。不可以间接调用,允许非局部返回
  3. noinline修饰符
    标记lambda参数,让被标记的参数不被内联。可以间接调用,不能非局部返回(跟(1.)一样)。
  4. crossinline修饰符
    内联该参数,但由于return作用域有迷惑性,故直接禁止。可以间接调用,不能非局部返回

观察发现,只有inline可以局部返回,不能间接调用。

此外,可以去学一下**“泛型实化”reified关键字**,可以配合inline使用,函数调用时确定泛型的类型,岂不美哉?


如果有帮助记得点个赞收藏或者点个关注,谢谢:)🙏

  • 16
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值