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

2,778 阅读8分钟

前言

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

1.局部返回

在介绍inline关键字之前,我们有必要先来介绍下Kotlin中局部返回的概念。那么什么是局部返回呢?在Kotlin中非内联的Lambda表达式是不支持使用裸return的,在非内联的Lambda表达式内部我们只能使用@标签限制的return来进行局部返回。如果我们强行在一个Lambda表达式中使用裸return,编译器会报语法错误。如下示例代码,我们在main()函数中调用高阶函数normal()的时候使用裸return:

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

inline2.png 通常这种写法,我们称为返回到标签。当一个高阶函数在调用的时候,其函数类型参数初始化的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按钮。

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

7.pngmain()函数中我们仅仅是将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

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

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

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

4.crossinline

在实际开发的场景中,一些内联函数可能会将自身拥有的函数类型参数实例的调用放在来自另一个上下文作用域的Lambda表达式或者一个匿名类中。例如我们需要将UI代码放在主线程中去执行,我们通常会这么写:

inline11.png 我们给View添加一个postDelayed()的扩展函数:

inline16.png 这种不在内联函数的函数体内部直接调用函数类型参数的实例,而是将其放在一个拥有另一个上下文的Lambda表达式或匿名类中,我们通常称之为间接调用。而间接调用一个函数类型参数的实例是不支持在该函数类型初始化的Lambda表达式中使用裸return的。但是上面我们又说到我们可以在内联函数的Lambda表达式中使用裸return。两者在语法上产生了冲突,Kotlin编译器直接提示了语法错误,不允许这么调用。但我们的业务场景又常常会遇到这种调用的情况。这时crossinline关键字就派上用场了,它就像一个契约告诉编译器,对于这种出现间接调用函数类型实例的内联函数,一定不会在调用该内联函数的Lambda表达式中使用裸return,当我们给参数block加上crossinline关键字以后,Kotlin编译器不会在报语法错误了。

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

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

inline14.png

总结

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