5.7.5 内联函数

原文链接: https://kotlinlang.org/docs/inline-functions.html

5.7.5 内联函数

使用高阶函数会带来一定的运行时开销:每个函数都是一个对象,并且会捕获闭包。闭包是可以在函数体中被访问的变量作用域。内存分配(函数对象和类的分配)以及虚调用都会带来运行时开销。

但事实证明,在很多情况下这种开销可以通过内联 lambda 表达式来消除。下面这些函数就是这种情况的典型例子。lock() 函数很容易在调用处内联。请看下面的情况:

1
lock(l) { foo() }

编译器可以不再为参数创建函数对象并生成调用,而是生成如下代码:

1
2
3
4
5
6
l.lock()
try {
    foo()
} finally {
    l.unlock()
}

要让编译器这么做,请给 lock() 函数加上 inline 修饰符:

1
inline fun <T> lock(lock: Lock, body: () -> T): T { ... }

inline 修饰符既影响函数本身,也影响传给它的 lambda:它们都会被内联到调用处。

内联可能会导致生成的代码变大。不过,只要以合理的方式进行(避免内联大型函数),它就能在性能上带来回报,尤其是在循环中"多态"的调用处。

noinline

如果你不希望传给内联函数的所有 lambda 都被内联,可以用 noinline 修饰符标记其中一些函数参数:

1
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }

可内联的 lambda 只能在内联函数内部调用,或作为可内联实参传递。而 noinline lambda 则可以用任何方式处理,包括存入字段或到处传递。

注意: 如果内联函数既没有可内联的函数参数,也没有具体化类型参数,编译器会发出警告,因为内联这类函数几乎不可能带来好处(如果你确信需要内联,可以用 @Suppress("NOTHING_TO_INLINE") 注解来抑制该警告)。

非局部跳转表达式

返回

在 Kotlin 中,只能用普通的、不加限定的 return 退出具名函数或匿名函数。要退出 lambda,请使用标签。在 lambda 中禁止使用裸 return,因为 lambda 不能使外层函数 return:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun ordinaryFunction(block: () -> Unit) {
    println("hi!")
}
fun foo() {
    ordinaryFunction {
        return // 错误:无法在这里让 `foo` 返回
    }
}
fun main() {
    foo()
}

但如果 lambda 所传入的那个函数是内联的,那么这个返回也可以被内联。因此下面的写法是允许的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
inline fun inlined(block: () -> Unit) {
    println("hi!")
}
fun foo() {
    inlined {
        return // OK:该 lambda 会被内联
    }
}
fun main() {
    foo()
}

这类返回(位于 lambda 中,却退出外层函数)称为非局部返回。这种结构通常出现在循环中,而内联函数常常包裹着循环:

1
2
3
4
5
6
fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if (it == 0) return true // 从 hasZeros 返回
    }
    return false
}

注意,有些内联函数可能不会直接从函数体调用传给它们的 lambda,而是从其他执行上下文(例如局部对象或嵌套函数)中调用。在这种情况下,lambda 中也不允许使用非局部控制流。要表明内联函数的 lambda 参数不能使用非局部返回,请给该 lambda 参数加上 crossinline 修饰符:

1
2
3
4
5
6
inline fun f(crossinline body: () -> Unit) {
    val f = object: Runnable {
        override fun run() = body()
    }
    // ...
}

break 与 continue

与非局部 return 类似,对于传给包裹着循环的内联函数的 lambda,你也可以在其中使用 break 和 continue 跳转表达式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun processList(elements: List<Int>): Boolean {
    for (element in elements) {
        val variable = element.nullableMethod() ?: run {
            log.warning("Element is null or invalid, continuing...")
            continue
        }
        if (variable == 0) return true
    }
    return false
}

具体化类型参数

有时你需要访问作为参数传入的类型:

1
2
3
4
5
6
7
8
fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
    var p = parent
    while (p != null && !clazz.isInstance(p)) {
        p = p.parent
    }
    @Suppress("UNCHECKED_CAST")
    return p as T?
}

这里你沿着树向上遍历,并用反射检查某个节点是否属于特定类型。这本身没问题,但调用处不太美观:

1
treeNode.findParentOfType(MyTreeNode::class.java)

更好的解决方案是直接给这个函数传入一个类型。可以这样调用:

1
treeNode.findParentOfType<MyTreeNode>()

为了支持这一点,内联函数支持具体化类型参数,因此你可以这样写:

1
2
3
4
5
6
7
inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}

上面的代码用 reified 修饰符限定类型参数,使它在函数内部可以访问,几乎就像普通类一样。由于函数被内联,不需要反射,而且 !is 和 as 这样的普通运算符现在也可以使用。此外,你可以像上面那样调用该函数:myTree.findParentOfType<MyTreeNodeType>()。

尽管很多情况下不需要反射,但你仍然可以在具体化类型参数上使用它:

1
2
3
4
5
inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
    println(membersOf<StringBuilder>().joinToString("\n"))
}

普通函数(未标记为 inline)不能有具体化参数。没有运行时表示的类型(例如非具体化的类型参数,或像 Nothing 这样的虚构类型)不能用作具体化类型参数的实参。

内联属性

inline 修饰符可以用在没有幕后字段的属性的访问器上。你可以分别标注某个属性的访问器:

1
2
3
4
5
6
val foo: Foo
    inline get() = Foo()

var bar: Bar
    get() = ...
    inline set(v) { ... }

你也可以标注整个属性,从而把它的两个访问器都标记为 inline:

1
2
3
inline var bar: Bar
    get() = ...
    set(v) { ... }

在调用处,内联访问器会像普通内联函数一样被内联。

public API 内联函数的限制

当内联函数是 public 或 protected,但不是 private 或 internal 声明的一部分时,它被视为某个模块的公开 API。它可以在其他模块中调用,并在这些调用处被内联。

如果调用模块在声明内联函数的模块发生变更后没有重新编译,就会带来二进制不兼容的风险。

为消除模块的非公开 API 变更可能引入的这类不兼容风险,公开 API 内联函数不允许在其函数体中使用非公开 API 的声明,即 private 和 internal 声明及其组成部分。

internal 声明可以用 @PublishedApi 注解标注,从而允许在公开 API 内联函数中使用它。当 internal 内联函数被标记为 @PublishedApi 时,它的函数体也会像公开的一样受到检查。