内联(inline)函数#

内联函数是在普通函数基础上加上 inline 关键字,使得它们在编译期间会在被调用处原地展开,因此并不会发生函数调用。

由于省略了一次函数调用,最直接的受益就是(少许)性能提升。

下面这段代码定义了一个 inline 函数, 并且在 main 中调用它。可以看到用法与普通函数一样

inline fun add(a: Int, b: Int) {
    return a + b
}

fun main() {
    val first = 1
    val second = 3
    val third = add(first, second)
}

编译后上面的 add 会被调用者原地展开, 因此会变成类似下面这样:

fun main() {
    val first = 1
    val second = 3
    val third = first + second
}

使用场景#

第一个小场景是把一些大量使用的函数使用 inline 实现,可以稍微提升性能, 例如对集合操作: numbers.map { it * 2 }; 这些 map、filter 等操作在 kotlin 里面就是用 inline 函数实现, 下面是伪代码

class ArrayList<T> {
    ...
    public inline fun map(transform: (T) -> R): List<R> {
        val target = mutableListOf<R>()
        for (item in this) {
            target.add(transform(item))
        }
        return target
    }
}

因此我们每次对集合使用 filter/map 等操作的时候,调用都会在原地展开为 for 循环(transform 函数本身也有办法能展开)。

如果只是节省一次函数调用,难道我们就差这一次函数调用的性能吗? 那自然也不是的,导致这个功能初看似乎很鸡肋, 甚至官方也不鼓励通过 inline 提升性能。

终于进入正题了,下面介绍第二个使用场景: inline 函数引申出来的神奇用法: 规避泛型擦除!

例如,下面是 mongoTemplate 数据库的查询接口的 java 代码:

public <T> T findOne(Query query, Class<T> entityClass) {
    ...
}

为什么这个接口不能少一个参数,写成:

public <T> T findOne(Query query) {
    Class<T> entityClass = T.class;
    ...
}

还不是因为万恶的泛型擦除!

但是这个功能在 kotlin 里面可以使用 inline 实现了! 只需要顺带声明泛型的时候加上 reified 关键字。

public inline <reified T> findOne(Query query): T {
    Class<T> entityClass = T.class;
    ...
}

// 使用 findOne
void main() {
    val user = findOne<User>(query);
}

kotlin 基于 jvm, 也存在泛型擦除问题,但是 findOne 压根不是一个函数,它会在调用的地方展开, 所以能在编译期看到上下文里面的那个泛型! 上面那段代码会在 main 里面展开成类似这样的伪代码:

void main() {
    val user = {
        Class<T> entityClass = User.class;
        ...
    }
}

kotlin 可以与 java 无缝调用,但是仍然有很多开源 java 库(例如 spring, jackson)主动适配 kotlin, 一个主要适配点,就是通过 inline 这个特性,规避泛型擦除,提升接口易用性。

注意点#

class 中的 inline 函数需要关注权限问题。例如 public inline 函数不能访问 private 成员和方法, 会导致无法展开。

例如,下面的 isTeenager 函数访问了私有的 age:

class User {
    private val age: Int

    public inline fun isTeenager(): Boolean {
        // 访问了 私有成员 age
        return this.age < 18
    }
}

导致下面的函数

fun main() {
    val user: User = ...
    val isTeen = user.isTeenager()
}

会被展开成下面这样,存在无法访问 私有变量 的问题。

fun main() {
    val user: User = ...
    // 没有权限访问 age.
    val isTeen = user.age < 18
}