Kotlin语法基础篇五:inline、noinline、crossinline

前言

在前两篇文章中我们介绍到Kotlin中的函数高阶函数和Lambda表达式。这篇文章我们来讲解Kotlin源码中常见的三个关键字inline、noinline、crossinline的使用,当然这是在掌握了前两篇文章的基础上来展开介绍的。如果对Kotlin中的函数高阶函数和Lambda表达式不熟悉的读者,可以看下这两篇文章。下面我们开始本篇文章的学习。 

1.局部返回

在介绍inline关键字之前,我们有必要先来介绍下Kotlin中局部返回的概念。那么什么是局部返回呢?

在Kotlin中非内联的Lambda表达式是不支持使用裸return的,在非内联的Lambda表达式内部我们只能使用标签限制的return来进行局部返回。如果我们强行在一个Lambda表达式中使用裸return,编译器会报语法错误。如下示例代码,我们在main函数中调用高阶函数normal的时候使用裸return:

 而当我们使用@标签限制的return,编译器不再提示语法错误。

通常这种写法,我们称为返回到标签。当一个高阶函数在调用的时候,其函数类型初始化的Lambda表达式通常都会有一个默认的隐式标签,而这个隐式标签通常都是按照它外部的函数名来命名的。如上示例代码,我们在调用高阶函数normal的时候,在lambda表达式内部使用return@normal来完成包裹它的外层函数normal的返回。当然我们也可以自己定义标签的名称。 

fun main() {
    normal test@{
        println("called normal")
        return@test
    }
}

fun normal(block:() -> Unit) {
    block()
}

可以看到我们只需要在lambda表达式的花括号外使用@符号,并在@符号前加上我们自定义的标签名,这样我们就可以给一个Lambda表达式显示的声明一个标签名。如上示例代码,我们给normal函数声明了一个test的标签名,这样我们在Lambda表达式中就可以使用我们自定义的标签来完成当前高阶函数的局部返回。
为了验证带有@标签限制的return只是局部返回,我们在上面main函数的首行和尾行各打印一行代码,如下:

fun main() {
    println("main called start")
    normal {
        println("normal called start")
        return@normal
        println("normal called end")
    }
    println("main called end")
}

fun normal(block:() -> Unit) {
    block()
}

// 输出
main called start
normal called  start
main called end

从上述代码的打印结果我们可以看到retrun@normal仅仅只是对直接包裹它的外层函数normal进行了返回,而并没有对最外层的main函数进行返回。 

2.inline 

在Kotlin中使用关键字inline修饰一个函数的时候,我们就称这个函数是内联函数。内联函数不仅可以内联自己函数体内部的代码,还可以内联函数体内部函数体的代码(Lambda表达式中的代码)。下面我们先来看一下示例代码:

fun main() {
   normal { println("normal called") }
}

fun normal(block:() -> Unit) {
    println("normal started")
    block()
    println("normal end")
}

我们知道,Kotlin代码最终还是要编译成Java字节码的。在Android Studio中选择Tools -> Kotlin -> Show Kotlin Bytecode,在右边弹出的方框中,我们点击Decompile按钮。

在上述截图中标记的2中我们可以看到,Lambda表达式在Java中其实是用匿名内实现的。这就代表我们每调用一次高阶函数normal就会创建一个Funciton的匿名类,这在内存上会造成额外的销。
当我们使用inline关键字来修饰normal函数的时候,我们再来看一下反编译成Java字节码的情况: 

在main函数中我们仅仅是将3处的代码替换到了2处。并没有创建额外的匿名类。我们将上面的代码稍作更改如下:  

fun main() {
   println("main started")
   normal {
       println("normal called")
       return
   }
   println("main end")
}

inline fun normal(block:() -> Unit) {
    block()
}

// 输出
main started
normal called

可以看到当我们使用inline关键字修饰normal函数的时候,在main函数中我们可以直接在normal函数中使用裸return来完成最外层函数main的返回。
到这里我们就可以总结一下inline关键字的优点和缺点了:
1. 对于普通的函数,使用内联函数是完全没有必要的,只是减少了一次方法栈的调用,这种优化可以忽略
2. 对于带有函数类型参数的高阶函数,我们使用inline关键字修饰的内联函数,来节省Lambda表达式在调用的地方创建匿名类带来的内存开销
3. 由于内联函数,不仅可以内联自己内部的代码,还可以内联内部的内部中的代码(Lambda表达式中的代码)。在调用的地方仅仅只是代码的替换,我们可以在Lambda表达式中,直接使用裸return来完成最外层函数的返回。这种返回(位于 lambda 表达式中,但退出包含它的函数)我们称之为非局部返回。
4. 如果需要内联的函数代码逻辑过于复杂,调用该函数又比较频繁,则会导致调用该内联函数的地方出现代码臃肿的情况。

3.noinline 

在Kotlin中noinline关键字总是和inline关键字成对的出现。翻译成中文的意思就是禁用内联,我们先来看一下,如下的代码场景:

我们在inline.kt的文件中定义了两个高阶函数,normal和simple。其中normal函数拥有两个函数类型的参数block1和block2。simple函数拥有一个和normal函数中block2类型相同的函数类型参数block。我们在normal函数中调用了simple函数,并将block2函数类型的参数传递给了simple函数,编译器提示了语法错误。按常规的函数调用来说,这两个函数类型是一致的,按理来说可以正常传递,那么为什么Kotlin编译器却给出了语法错误的提示呢?
事实上内联函数的函数类型参数在编译的时候是没有具体的参数类型的,因为它只是进行代码的替换。所以在Kotlin中有这么一个规定,内联函数的函数类型参数只能传递给内联函数。而noinline关键字在这种场景下就可以派上用场了:

当我们给normal函数的函数类型参数block2加上noinline关键字来禁用其内联。这个时候在我们的高阶函数normal中的block2参数已经被取消了内联的资格,我们再将block2传递给simple函数,编译器就不会再报语法错误提示了。 

4.crossinline 

在一些实际开发的场景中,内联函数内部可能会将我们函数类型实例的调用放在一个外部上下文作用域的Lambda表达式中。关于带有上下文作用域的Lambda表达式,我们已经在上一篇文章高阶函数和Lambda表达式中详细介绍,这里就不再介绍了。例如我们需要将UI代码放在主线程中去执行,我们通常会这么写:

再例如我们给View添加一个postDelayed的扩展函数: 

由上面的截图我们可以看到两个示例中编译器都给出了语法错误的提示。一种是使用带有接受者的函数类型参数,在该函数类型初始化的Lambda式中将上下文切换到协程中。另外一种则是通过创建匿名类的方式,匿名类的花括号中自然是当前匿名类的上下文作用域。而在Kotlin中不允许在内联函数内部中,在一个拥有外部作用域的Lambda表达式中使用裸return。而上面我们在介绍到inline关键字的时候,我们又说到内联函数支持局部返回,可以在调用该内联函数内部使用裸return。这不是矛盾了吗?对于这种语法上的冲突,Kotlin编译器直接提示了语法错误,不允许这么调用。但我们的业务场景又常常会遇到这种调用的情况。于是Koltin给我们提供了crossinline关键字,它就像一个契约告诉编译器,我一定不会在这种内联函数中拥有外部上下文作用域的Lambda表达式中使用裸return,当我们给参数block加上crossinline关键字以后,Kotlin编译器不会在报语法错误了。

但同时我们也向Kotlin编译器保证了,不会在调用该内联函数的时候,在该内联函数的Lmabda表达式中使用裸return。如果我们此时再去在调用该内联函数的Lambda表达式中使用裸return了,编译器还是会提示语法错误:

现在我们已经没有办法在调用内联函数runInMainThead的Lambda表达式中使用裸return了,只能使用@标签限制的return。   

总结: 

关于inline、noinline、crossinline关键字的使用到这里就介绍完了。熟练的掌握了这一节的内容,对于我们阅读源码和实际开发会有很大的帮助。下篇文章笔者打算结合一下自己在开发中对扩展函数和高阶函数的运用展开介绍,我们下期再见!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值