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的返回。当然我们也可以自己定义标签的名称。

kotlin 复制代码
fun main() {
    normal test@{
        println("called normal")
        return@test
    }
}

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

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

kotlin 复制代码
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表达式中的代码)。下面我们先来看一下示例代码:

kotlin 复制代码
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处。并没有创建额外的匿名类。我们将上面的代码稍作更改如下:

kotlin 复制代码
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代码放在主线程中去执行,我们通常会这么写:

而这个时候编译给出了语法错误的提示。这种将函数类型实例的调用嵌套在另外一个Lambda表达式中,我们通常称为间接调用。 上面我们说到用inline关键字修饰的内联函数,支持非局部返回,也就是上面我们说的支持在Lambda表达式中使用裸return,现在问题来了。在这种间接调用函数类型实例的状态下,我们在调用该高阶函数的Lambda的表达式中使用裸return,到底是返回它外层函数的调用,还是再外层函数的调用呢?

对于这种语法上的冲突,Kotlin编译器直接提示语法错误了,不允许这么调用。但我们的业务场景又常常会遇到这种间接调用的情况。于是Koltin给我们提供了crossinline关键字,它就像一个契约告诉编译器,我一定不会在这种间接调用函数类型实例的Lambda表达式中使用裸return,当我们给参数block加上crossinline关键字以后,Kotlin编译器不会在报语法错误了。

但同时我们也向Kotlin编译器保证了,不会在调用该内联函数的时候,在Lmabda表达式中使用裸return。如上面代码示例,我们在内联函数runInMainThead方法中已经没有办法使用裸return了,只能使用标签限制的return。

总结

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

相关推荐
F-2H1 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
gqkmiss2 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
mmsx3 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
m0_748247554 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255025 小时前
前端常用算法集合
前端·算法
真的很上进5 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203985 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2345 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
众拾达人5 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
如若1236 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python