Kotlin内联函数

1,873 阅读5分钟
原文链接: www.jianshu.com

Kotlin里使用关键 inline 来表示内联函数,那么到底什么是内联函数呢,内联函数有什么好处呢?

1. 什么是内联inline?

在 Java 里是没有内联这个概念的,所有的函数调用都是普通方法调用,如果了解 Java 虚拟机原理的,可以知道 Java 方法执行的内存模型是基于 Java 虚拟机栈的:每个方法被执行的时候都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧入栈、出栈的过程。

也就是说每调用一个方法,都会对应一个栈帧的入栈出栈过程,如果你有一个工具类方法,在某个循环里调用很多次,那就会对应很多次的栈帧入栈、出栈过程。这里首先要记住的一点是,栈帧的创建及入栈、出栈都是有性能损耗的。下面以一个例子来说明,看段代码片段:

fun test() {
    //多次调用 sum() 方法进行求和运算
    println(sum(1, 2, 3))
    println(sum(100, 200, 300))
    println(sum(12, 34))
    //....可能还有若干次
}

/**
 * 求和计算
  */
fun sum(vararg ints: Int): Int {
    var sum = 0
    for (i in ints) {
        sum += i
    }
    return sum
}

在测试方法 test() 里,我们多次调用了 sum() 方法。为了避免多次调用 sum() 方法带来的性能损耗,我们期望的代码类似这样子的:

fun test() {
    var sum = 0
    for (i in arrayOf(1, 2, 3)) {
        sum += i            
    }
    println(sum)
    
    sum = 0
    for (i in arrayOf(100, 200, 300)) {
        sum += i            
    }
    println(sum)
    
    sum = 0
    for (i in arrayOf(12, 34)) {
        sum += i            
    }
    println(sum)
}

3次数据求和操作,都是在 test() 方法里执行的,没有之前的 sum() 方法调用,最后的结果依然是一样的,但是由于减少了方法调用,虽然代码量增加了,但是性能确提升了。那么怎么实现这种情况呢,一般工具类有很多公共方法,我总不能在需要调用这些公共方法的地方,把代码复制一遍吧,内联就是为了解决这一问题。

定义内联函数:

inline fun sum(vararg ints: Int): Int {
    var sum = 0
    for (i in ints) {
        sum += i
    }
    return sum
}

如上所示,用关键字 inline 标记函数,该函数就是一个内联函数。还是原来的 test() 方法,编译器在编译的时候,会自动把内联函数 sum() 方法体内的代码,替换到调用该方法的地方。查看编译后的字节码,会发现 test() 方法里已经没了对 sum() 方法的调用,凡是原来代码里出现 sum() 方法调用的地方,出现的都是 sum() 方法体内的字节码了。

2. noinline

如果一个内联函数的参数里包含 lambda表达式,也就是函数参数,那么该形参也是 inline 的,举个例子:

inline fun test(inlined: () -> Unit) {...}

这里有个问题需要注意,如果在内联函数的内部,函数参数被其他非内联函数调用,就会报错,如下所示:

//内联函数
inline fun test(inlined: () -> Unit) {
    //这里会报错
    otherNoinlineMethod(inlined)   
}

//非内联函数
fun otherNoinlineMethod(oninline: () -> Unit) {
    
}

要解决这个问题,必须为内联函数的参数加上 noinline 修饰,表示禁止内联,保留原有函数的特性,所以 test() 方法正确的写法应该是:

inline fun test(noinline inlined: () -> Unit) {
    otherNoinlineMethod(inlined)
}

3. crossinline

首先来理解一个概念:非局部返回。我们来举个栗子:

fun test() {
    innerFun {      
        //return 会报错,非局部返回,直接退出 test() 函数。
        return@test     //局部返回,只退出 innerFun() 函数
    }
    
    //以下代码依旧会执行
    println("test...")
}

fun innerFun(a: () -> Unit) {
    a()
}

非局部返回我的理解就是返回到顶层函数,如上面代码中所示,默认情况下是不能直接 return 的,但是内联函数确是可以的。所以改成下面这个样子:

fun test() {
    innerFun {      
        return //非局部返回,直接退出 test() 函数。
    }
    
    //以下代码不会执行
    println("test...")
}

inline fun innerFun(a: () -> Unit) {
    a()
}

也就是说内联函数的函数参数在调用时,可以非局部返回,如上所示。那么 crossinline 修饰的 lambda 参数,可以禁止内联函数调用时非局部返回。

fun test() {
    innerFun {      
        return //这里这样会报错,只能 return@innerFun
    }
    
    //以下代码不会执行
    println("test...")
}

inline fun innerFun(crossinline a: () -> Unit) {
    a()
}

4. 具体化的类型参数 reified

这个特性我觉得特别牛逼,有了它可以少些好多代码。在 Java 中是不能直接使用泛型的类型的,但是在 Kotlin 中确可以。举个栗子:

inline fun <reified T: Activity> startActivity() {
    startActivity(Intent(this, T::class.java))
}

使用时直接传入泛型即可,代码简洁明了:

startActivity<MainActivity>()

5. 小结

网上很多学习教程对内联函数的讲解都是千篇一律,说实话刚开始很难理解。本文尝试着用最简单的例子,来讲清楚什么是内联函数。在Java中我们一般会有很多工具类、工具方法,在Kotlin中则没有了工具类一说,通常都是将工具方法设计成顶层的内联函数来使用。