【Kotlin】内联函数

文章目录

        • 内联函数
        • noinline: 避免参数被内联
        • 非局部返回
        • 使用标签实现Lambda非局部返回
          • 为什么要设计noinline
        • crossinline
        • 具体化参数类型

Kotlin中的内联函数之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。然而,在Java中我们似乎并不需要特别关注这个问题,因为在Java 7之后,JVM引入了一种叫做 invokedynamic的技术,它会自动帮助我们做Lambda优化。但是为什么Kotlin要引入内联函数这种手动的语法呢? 这主要还是因为Kotlin要兼容Java 6。

在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类(也就是说我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式)。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的匿名类对象。

可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。

内联函数

Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。所以内联函数的工作原理并不复杂,就是Kotlin编译器会将内敛函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。 。

看看Kotlin的内联函数是具体如何操作的:

kotlin 复制代码
fun main(args: Array<String>) {
    foo {
        println("dive into Kotlin...")
    }
}

fun foo(block: () -> Unit) {
    println("before block")
    block()
    println("end block")
}

首先,我们声明了一个高阶函数foo,可以接受一个类型为() -> Unit的Lambda,然后在main函数中调用它。以下是通过字节码反编译的相关Java代码:

java 复制代码
public static final void main(@NotNull String[] args) {
    Intrinsics.checkParameterIsNotNull(args, "args");
    foo((Function0)null.INSTANCE);	        
}

public static final void foo(@NotNull Function0 block) {
    Intrinsics.checkParameterIsNotNull(block, "block");
    String var1 = "before block";
    System.out.println(var1);
    block.invoke();
    var1 = "end block";
    System.out.println(var1);
}

据我们所知,调用foo就会产生一个Function()类型的block类,然后通过invovke方法来执行,这会增加额外的生成类和调用开销。现在,我们给foo函数加上inline修饰符,如下:

kotlin 复制代码
inline fun foo(block: () -> Unit) {
    println("before block")
    block()
    println("end block")
}

再来看看相应的Java代码:

java 复制代码
public static final void main(@NotNull String[] args) {
    Intrinsics.checkParameterIsNotNull(args, "args");
    String va1 = "before block";
    System.out.println(var1);
    // block函数体在这里开始粘贴
    String var2 = "dive into Kotlin...";
    System.out.println(var2);
    // block函数体在这里结束粘贴
    var1 = "end block";
    System.out.println(var1);
}

public static final void foo(@NotNull Function0 block) {
    Intrinsics.checkParameterIsNotNull(block, "block");
    String var2 = "before block";
    System.out.println(var2);
    block.invoke();
    var2 = "end block";
    System.out.println(var2);
}

foo函数体代码及被调用的Lambda代码都粘贴到了相应调用的位置。试想下,如果这是一个工程中公共的方法,或者被嵌套在一个循环调用的逻辑体中,这个方法势必会被调用很多次。通过inline的语法,我们可以彻底消除这种额外调用,从而节省了开销。

内联函数典型的一个应用场景就是Kotlin的集合类。如果你看过Kotlin的集合类API文档或者源码实现就会发现,集合函数式API,如map、filter都被定义成内联函数,如:

kotlin 复制代码
inline fun <T, R> Array<out T>.map {
    transform: (T) -> R
}: List<R>

inline fun <T> Array<out T>.filter {
    predicate: (T) -> Boolean
}: List<T>

这个很容易理解,由于这些方法都接收Lambda作为参数,同时都需要对集合元素进行遍历操作,所以把相应的实现进行内联无疑是非常适合的。

但是内联函数不是万能的,以下情况我们应避免使用内联函数:

  • 由于JVM对普通的函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。
  • 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。
  • 一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非你把他们声明为internal。
noinline: 避免参数被内联

通过上面的例子我们已经知道,如果在一个函数的开头加上inline修饰符,那么它的函数体及Lambda参数都会被内联。然而现实中的情况比较复杂,有一种可能是函数需要接受多个参数,但我们只想对其中部分Lambda参数内联,其他的则不内联,这个又该如何处理?

解决这个问题也很简单,Kotlin在引入inline的同时,也新增了noinline关键字,我们可以把它加在不想要被内联的参数开头,该参数便不会具有内联的效果:

kotlin 复制代码
fun main(args: Array<String>) {
    foo ( {
        println("I am inlined...")	     	
    }, {
        println("I am not inlined...")
    })
}

inline fun foo(block1: () -> Unit, noinline block2: () -> Unit) {
    println("before block")
    block1()
    block2()
    println("end block")
}

同样的方法,再来看看反编译的Java版本:

java 复制代码
public static final void main(@NotNull String[] args) {
    Intrinsics.checkParameterIsNotNull(args, "args");
    Function0 block2$iv = (Function0)null.INSTANCE;
    String var2 = "before block";
    System.out.println(var2);
    // block1 被内联了
    String var3 = "I am inlined...";
    System.out.println(var3);
    // block2 还是原样
    block2$iv.invoke();
    System.out.println(var2);
}
public static final void foo(@NotNull Function0 block1, @NotNull Function0 block2) {
    Intrinsics.checkParameterIsNotNull(block1, "block1");
    Intrinsics.checkParameterIsNotNull(block2, "block2");
    String var3 = "before block";
    System.out.println(var3);
    block1.invoke();
    block2.invoke();
    var3 = "end block";
    System.out.println(var3);
}

可以看出,foo函数的block2参数在带上noinline之后,反编译后的Java代码中并没有将其函数体代码在调用处进行替换。

非局部返回

Kotlin中的内联函数除了优化Lambda开销之外,还带来了其他方面的特效,典型的就是非局部返回和具体化参数类型。我们先来看下Kotlin如何支持非局部返回。

以下是我们常见的局部返回的例子:

kotlin 复制代码
fun main(args: Array<String>) {
    foo()
}
fun localReturn() {
    return
}
fun foo() {
    println("before local return")
    localReturn()
    println("after local return")
    return
}
// 运行结果
before local return
after local return

正如我们所熟知的,localReturn执行后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。我们再把这个函数换成Lambda表达式的版本:

kotlin 复制代码
fun main(args: Array<String>) {
    foo { return }
}
fun foo(returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return 
}
// 运行结果
Error:(2, 11)Kotlin: 'return' is not allowed here

这时,编译器报错了,就是说在Kotlin中,正常情况下Lambda表达式不允许存在return关键字。这时候,内联函数又可以排上用场了。我们把foo进行内联后再试试看:

kotlin 复制代码
fun main(args: Array<String>) {
    foo { return }
}
inline fun foo(returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}
// 运行结果
before local return 

编译顺利通过了,但结果与我们的局部返回效果不同,Lambda的return执行后直接让foo函数退出了执行。如果你仔细考虑一下,可能很快就想出了原因。因为内联函数foo的函数体及参数Lambda会直接替代具体的调用。所以实际产生的代码中,retrurn相当于是直接暴露在main函数中,所以returning()之后的代码自然不会执行,这个就是所谓的非局部返回。

使用标签实现Lambda非局部返回

另外一种等效的方式,是通过标签利用@符号来实现Lambda非局部返回。同样以上的例子,我们可以在不声明inline修饰符的情况下,这么做来实现相同的效果:

kotlin 复制代码
fun main(args: Array<String>) {
    foo { return@foo }
}
fun foo(returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}
// 运行结果
before local return

非局部返回尤其在循环控制中显得特别有用,比如Kotlin的forEach接口,它接收的就是一个Lambda参数,由于它也是一个内联函数,所以我们可以直接在它调用的Lambda中执行return退出上一层的程序。

kotlin 复制代码
fun hasZeros(list: List<Int>): Boolean {
    list.forEach {
        if (it == 0) return true // 直接返回foo函数结果
    }
    return false
}
为什么要设计noinline

这里我已经蒙了,前面已经说了内联函数的好处,那为什么Kotlin还要提供一个noinline关键字来排除内联功能呢?

这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。

非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。

另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。为了说明这个问题,我们来看下面的例子:

kotlin 复制代码
fun printString(str: String, block: (String) -> Unit) {
    println("printString begin")
    block(str)
    println("printString end")
}

fun main() {
    println("main start")
    val str = ""
    printString(str) { s ->
        println("lambda start")
        if (s.isEmpty()) return@printString
        println(s)
        println("lambda end")
    }
    println("main end")
}

这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。现在我们就刚好传入一个空的字符串参数,运行程序,打印结果如下:

复制代码
main start
printString begin
lambda start
printString end
main end

可以看到,除了Lambda表达式中return@printString语句之后的代码没有打印,其他的日志是正常打印的,说明return@printString确实只能进行局部返回。但是如果我们将printString()函数声明成一个内联函数,那么情况就不一样了,如下所示:

kotlin 复制代码
inline fun printString(str: String, block: (String) -> Unit) {
    println("printString begin")
    block(str)
    println("printString end")
}

fun main() {
    println("main start")
    val str = ""
    printString(str) { s ->
        println("lambda start")
        if (s.isEmpty()) return
        println(s)
        println("lambda end")
    }
    println("main end")
}

现在printString()函数变成了内联函数,我们就可以在Lambda表达式中使用return关键字了。此时的return代表的是返回外层的调用函数,也就是main()函数,如果想不通为什么的话,可以回顾一下在上一小节中学习的内联函数的代码替换过程。现在重新运行一下程序,打印结果如下:

复制代码
main start
printString begin 
lambda start

可以看到,不管是main()函数还是printString()函数,确实都在return关键字之后停止执行了,和我们所预期的结果一致。

将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况。观察下面的代码示例:

kotlin 复制代码
inline fun runRunnable(block: () -> Unit) {
    val runnable = Runnable {
        block()
    }
    runnable.run()
}

这段代码在没有加上inline关键字声明的时候绝对是可以正常工作的,但是在加上inline关键字之后就会提示如下:

这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。

而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。

也就是说,如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。

那么是不是在这种情况下就真的无法使用内联函数了呢?也不是,比如借助crossinline关键字就可以很好地解决这个问题:

kotlin 复制代码
inline fun runRunnable(crossinline block: () -> Unit) {
    val runnable = Runnable {
        block()
    }
    runnable.run()
}

可以看到,这里在函数类型参数的前面加上了crossinline的声明,代码就可以正常编译通过了。

那么这个crossinline关键字又是什么呢?前面我们已经分析过,之所以会提示上面所示的错误,就是因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像一个契约,它用于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问题也就巧妙地解决了。

声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了,但是仍然可以使用return@runRunnable的写法进行局部返回。总体来说,除了在return关键字的使用上有所区别之外,crossinline保留了内联函数的其他所有特性。

crossinline

值得注意的是,非局部返回虽然在某些场合下非常有用,但可能也存在危险。因为有时候,我们内联的函数所接收的Lambda参数常常来自于上下文其他地方。为了避免带有return的Lambda参数产生破坏,我们还可以使用crossinline关键字来修饰该参数,从而杜绝此类问题的发生。就像这样子:

kotlin 复制代码
fun main(args: Array<String>) {
    foo { return }
}
inline fun foo(crossinline returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}
// 运行结果
Error: (2, 11) Kotlin: 'return' is not allowed here
具体化参数类型

除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型。Kotlin与Java一样,由于运行时的类型擦除,我们并不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型。我们可以用reified修饰符来实现这一效果。

kotlin 复制代码
fun main(args: Array<String>) {
    getType<Int>()
}
inline fun <reified T> getType() {
    print(T::class)
}
// 运行结果
class kotlin.Int

这个特性在Android开发中也格外有用。比如在Java中,当我们要调用startActivity时,通常需要把具体的目标视图类作为一个参数。然而,在Kotlin中,我们可以用reified来进行简化:

kotlin 复制代码
inline fun <refied T : Activity> Activity.startActivity() {
    startActivity(Intent(this, T::class.java))
}

这样,我们进行视图导航就非常容易了,如:

kotlin 复制代码
startActivity<DetailActivity>()
相关推荐
Scott9999HH1 天前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社1 天前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海1 天前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖1 天前
JDK 26 新特性详解
java·开发语言
马优晨1 天前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区1 天前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大1 天前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai1 天前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜1 天前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa1 天前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio