Kotlin 知识梳理系列文章
Kotlin 知识梳理(1) - Kotlin 基础
Kotlin 知识梳理(2) - 函数的定义与调用
Kotlin 知识梳理(3) - 类、对象和接口
Kotlin 知识梳理(4) - 数据类、类委托 及 object 关键字
Kotlin 知识梳理(5) - lambda 表达式和成员引用
Kotlin 知识梳理(6) - Kotlin 的可空性
Kotlin 知识梳理(7) - Kotlin 的类型系统
Kotlin 知识梳理(8) - 运算符重载及其他约定
Kotlin 知识梳理(9) - 委托属性
Kotlin 知识梳理(10) - 高阶函数:Lambda 作为形参或返回值
Kotlin 知识梳理(11) - 内联函数
Kotlin 知识梳理(12) - 泛型类型参数
一、本文概要
本文是对<<Kotlin in Action>>
的学习笔记,如果需要运行相应的代码可以访问在线环境 try.kotlinlang.org,这部分的思维导图为:
二、内联函数
当我们使用lambda
表达式时,它会被正常地编译成匿名类。这表示每调用一次lambda
表达式,一个额外的类就会被创建,并且如果lambda
捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这会带来运行时的额外开销,导致使用lambda
比使用一个直接执行相同代码的函数效率更低。
如果使用inline
修饰符标记一个函数,在函数被调用的时候编译器并不会生成函数调用的代码,而是 使用函数实现的真实代码替换每一次的函数调用。
2.1 内联函数如何运作
当一个函数被声明为inline
时,它的函数体是内联的,也就是说,函数体会被直接替换到函数被调用地方,下面我们来看一个简单的例子,下面是我们定义的一个内联的函数:
inline fun inlineFunc(prefix : String, action : () -> Unit) {
println("call before $prefix")
action()
println("call after $prefix")
}
我们用如下的方法来使用这个内联函数:
fun main(args: Array<String>) {
inlineFunc("inlineFunc") {
println("HaHa")
}
}
运行结果为:
>> call before inlineFunc
>> HaHa
>> call after inlineFunc
最终它会被编译成下面的字节码:
fun main(args: Array<String>) {
println("call before inlineFunc")
println("HaHa")
println("call after inlineFunc")
}
lambda
表达式和inlineFunc
的实现部分都被内联了,由lambda
生成的字节码成了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的匿名类中。
传递函数类型的变量作为参数
在调用内联函数的时候,也可以传递函数类型的变量作为参数,还是上面的例子,我们换一种调用方式:
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
被调用的地方,因此我们不用担心性能问题。
假如我们像下面这样,连续调用filter
和map
两个操作:
println(people.filter{ it.age > 30 }.map(Person :: name))
这个例子使用了一个lambda
表达式和一个成员引用,filter
和map
函数都被声明为inline
函数,所以不会额外产生类或者对象,但是上面的代码会创建一个中间集合来保存列表过滤的结果。
2.4 决定何时将函数声明成内联
对于普通函数的调用,JVM
已经提供了强大的内联支持。它会分析代码的执行,并在任何通过内联能够带来好处的时候将函数调用内联。
带有lambda
参数的函数内联能带来好处:
- 节约了函数调用的开销,节约了为
lambda
创建匿名类,以及创建lambda
实例对象的开销。 -
JVM
目前并没有聪明到总是能够将函数调用内联。 - 内联使得我们可以使用一些不可能被普通
lambda
使用的特性,例如 非局部返回。
但是在使用inline
关键字的时候,还是应该注意代码的长度,如果你要内联的函数很大,将它的字节码拷贝到每一个调用点将会极大地增加字节码的长度。在这种情况下,你应该将那些与lambda
参数无关的代码抽取到一个独立的非内联函数中。
三、高阶函数中的控制流
当你使用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 知识梳理系列:
- Android 知识梳理目录://www.greatytc.com/p/fd82d18994ce
- 个人主页:http://lizejun.cn
- 个人知识总结目录:http://lizejun.cn/categories/