Kotlin 知识梳理(11) 内联函数

1、本文概要

本文是对<<Kotlin in Action>>的学习笔记,若是须要运行相应的代码能够访问在线环境 try.kotlinlang.org,这部分的思惟导图为: java

2、内联函数

当咱们使用lambda表达式时,它会被正常地编译成匿名类。这表示每调用一次lambda表达式,一个额外的类就会被建立,而且若是lambda捕捉了某个变量,那么每次调用的时候都会建立一个新的对象,这会带来运行时的额外开销,致使使用lambda比使用一个直接执行相同代码的函数效率更低。函数

若是使用inline修饰符标记一个函数,在函数被调用的时候编译器并不会生成函数调用的代码,而是 使用函数实现的真实代码替换每一次的函数调用性能

2.1 内联函数如何运做

当一个函数被声明为inline时,它的函数体是内联的,也就是说,函数体会被直接替换到函数被调用地方,下面咱们来看一个简单的例子,下面是咱们定义的一个内联的函数:学习

inline fun inlineFunc(prefix : String, action : () -> Unit) {
    println("call before $prefix")
    action()
    println("call after $prefix")
}
复制代码

咱们用以下的方法来使用这个内联函数:this

fun main(args: Array<String>) {
    inlineFunc("inlineFunc") {
        println("HaHa")
    }
}
复制代码

运行结果为:spa

>> call before inlineFunc
>> HaHa
>> call after inlineFunc
复制代码

最终它会被编译成下面的字节码:code

fun main(args: Array<String>) {
    println("call before inlineFunc")
    println("HaHa")
    println("call after inlineFunc")
}
复制代码

lambda表达式和inlineFunc的实现部分都被内联了,由lambda生成的字节码成了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的匿名类中。orm

传递函数类型的变量做为参数

在调用内联函数的时候,也能够传递函数类型的变量做为参数,仍是上面的例子,咱们换一种调用方式:cdn

fun main(args: Array<String>) {
    val call : () -> Unit = { println("HaHa") }
    inlineFunc("inlineFunc", call)
}
复制代码

那么此时最终被编译成的Java字节码为:对象

fun main(args: Array<String>) {
    println("call before inlineFunc ")
    action()
    println("call after inlineFunc")
}
复制代码

在这种状况,只有inlineFunc的实现部分被内联了,而lambda的代码在内联函数被调用点是不可用的。

在两个不一样的位置使用同一个内联函数

若是在两个不一样的位置使用同一个内联函数,可是用的是不一样的lambda,那么内联函数会在每个被调用的位置分别内联,内联函数的代码会被拷贝到使用它的两个不一样位置,并把不一样的lambda替换到其中。

2.2 内联函数的限制

鉴于内联的运做方式,不是全部使用 lambda 的函数均可以被内联。当函数被内联的时候,做为参数的lambda表达式的函数体会被 替换到最终生成的代码中

这将限制函数体中的lambda参数的使用:

  • 若是lambda参数 被调用,这样的代码能被容易地内联。
  • 若是lambda参数 在某个地方被保存起来,以便之后继续使用,lambda表达式的代码 将不能被内联,所以必需要 有一个包含这些代码的对象存在

通常来讲,参数若是 被直接调用或者做为参数传递 给另一个inline函数,它是能够被内联的,不然,编译器会 禁止参数被内联 并给出错误信息Illeagal usage of inline-parameter

例如,许多做用于序列的函数会返回一些类的实例,这些类表明对应的序列操做并接收lambda做为构造方法的参数,如下是Sequence.map函数的定义:

fun <T, R> Sequence<T>.map(transform : (T) -> R) : Sequence<R> {
    return TransformingSequence(this, transform);
}
复制代码

map函数没有直接调用做为transform参数传递进来的函数。而是将这个函数传递给一个类的构造方法,构造方法将它保存在一个属性当中。为了支持这一点,做为transform参数传递的lambda须要 被编译成标准的非内联表示法,即一个实现了函数接口的匿名类。

若是一个函数指望两个或更多的lambda函数,能够选择只内联其中一些参数,由于一个lambda可能会包含不少代码或者 以不容许内联的方式调用,接收这样的非内联lambda的参数,能够用noinline修饰符来标记它:

inline fun foo(inlined : () -> Unit, noinline noinlined : () -> Unit) {

}
复制代码

注意,编译器彻底支持 内联跨模块的函数或者第三方库定义的函数,也能够在 Java 中调用绝大部份内联函数

2.3 内联集合操做

大部分标准库中的集合函数都带有lambda参数。例如filter,它被声明为内联函数,这意味着filter函数,以及传递给它的lambda字节码会被内联到filter被调用的地方,所以咱们不用担忧性能问题。

假如咱们像下面这样,连续调用filtermap两个操做:

println(people.filter{ it.age > 30 }.map(Person :: name))
复制代码

这个例子使用了一个lambda表达式和一个成员引用,filtermap函数都被声明为inline函数,因此不会额外产生类或者对象,可是上面的代码会建立一个中间集合来保存列表过滤的结果。

2.4 决定什么时候将函数声明成内联

对于普通函数的调用,JVM已经提供了强大的内联支持。它会分析代码的执行,并在任何经过内联可以带来好处的时候将函数调用内联。

带有lambda参数的函数内联能带来好处:

  • 节约了函数调用的开销,节约了为lambda建立匿名类,以及建立lambda实例对象的开销。
  • JVM目前并无聪明到老是可以将函数调用内联。
  • 内联使得咱们可使用一些不可能被普通lambda使用的特性,例如 非局部返回

可是在使用inline关键字的时候,仍是应该注意代码的长度,若是你要内联的函数很大,将它的字节码拷贝到每个调用点将会极大地增长字节码的长度。在这种状况下,你应该将那些与lambda参数无关的代码抽取到一个独立的非内联函数中。

3、高阶函数中的控制流

当你使用lambda去替换像循环这样的命令式代码结构时,很快就会遇到return表达式的问题,把一个return语句放在循环的中间是很简单的事。可是若是将循环替换成一个相似filter的函数呢?

3.1 lambda 中的返回语句:从一个封闭的函数返回

下面,咱们经过一个例子来演示,在集合当中寻找名为Alice的人,找到了就直接返回:

data class Person(val name: String, val age: Int) val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}
复制代码

运行结果为:

>> Found !
复制代码

若是在lambda中使用return关键字,它会 从调用 lambda 的函数 中返回,并不仅是 从 lambda 中返回,这样的return语句叫作 非局部返回,由于它从一个比包含return的代码块更大的代码块中返回了。

须要注意的是,只有 以 lambda 做为参数的函数是内联函数 的时候才能从更外层的函数返回。在一个非内联的lambda中使用return表达式是不容许的,一个非内联函数能够把它的lambda保存在变量中,以便在函数返回之后能够继续使用,这个时候lambda想要去影响函数的返回已经太晚了。

3.2 从 lambda 中返回:使用标签返回

也能够在lambda表达式中使用局部返回,相似于for循环中的break表达式,它会终止lambda的执行,并接着从调用lambda的代码处执行。

要区分局部返回和非局部返回,要用到标签。想从一个lambda表达式处返回你能够标记它,而后在return关键字后面引用这个标签。

data class Person(val name: String, val age: Int) val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach label@{
        if (it.name == "Alice") return@label
    }
    println("Alice might be somewhere")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}
复制代码

运行结果为:

>> Alice might be somewhere
复制代码

另外一种选择是,使用lambda做为参数的函数的函数名能够做为标签,也就是上面的forEach,若是你显示地指定了lambda表达式的标签,再使用函数名做为标签没有任何效果。

3.3 匿名函数:默认使用局部返回

匿名函数是一种不一样的用于编写传递给函数的代码块的方式,先来看一个示例:

data class Person(val name: String, val age: Int) val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach(fun (person) {
        if (person.name == "Alice") return
        println("${person.name} is not Alice")
    })
}

fun main(args: Array<String>) {
    lookForAlice(people)
}
复制代码

运行结果为:

>> Bob is not Alice
复制代码

匿名函数和普通函数有相同的指定返回值类型的规则,代码块匿名函数 须要显示地指定返回类型,若是使用 表达式函数体,就能够省略返回类型。

在匿名函数中,不带return表达式会从匿名函数返回,而不是从包含匿名函数的函数返回,这条规则很简单:return从最近的使用fun关键字声明的函数返回。

  • lambda表达式没有使用fun关键字,因此lambda中的return从最外层的函数返回。
  • 匿名函数使用了fun,所以return表达式从匿名函数返回。

尽管匿名函数看起来和普通函数很类似,但它实际上是lambda表达式的另外一种语法形式而已。关于lambda表达式如何实现,以及在内联函数中如何被内联的讨论一样适用于匿名函数。


更多文章,欢迎访问个人 Android 知识梳理系列:

相关文章
相关标签/搜索