Kotlin中的 lambda 表达式

Kotlin 中 lambda 表达式的行为

在 Kotlin 中,如果一个 lambda 表达式没有使用在其代码块之外的变量,也就是不捕获外部变量,那处理起来很简单,直接创建一个该 lambda 表达式的唯一 Lambda 实例即可。

那如果 lambda 表达式内捕获了外部变量呢?就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
var sum = 0
repeat (100) {
process {
sum += 1
println(sum)
}
}
}

fun process(block: (() -> Unit)) {
block()
}

传给 process 的 lambda 表达式中使用了外部的 sum 变量,这会导致每次在不同上下文调用这个 process 函数时都重新创建一个 Lambda 实例,当在一个循环次数很大的循环体中时,会产生严重的性能损耗。

捕获过程伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Lambda implements Function0<Unit> {
final IntRef value;

Lambda(IntRef value) {
this.value = value;
}

public void invoke() {
value.element += 1
System.out.println(value.element);
}
}
// 并且原 sum 变量定义处会被编译器修改为 IntRef
// var sum = 0 将变为 IntRef sum = IntRef(); sum.element = 0

修改为 IntRef 的原因相信不难理解,因为 Int 类型是基本类型,如果程序员在 lambda 作用域中修改捕获变量的值,是无法反应在外部的,同理,在外部修改也无法反映在 Lambda 对象的内部,所以为了解决这个问题,会统一将基本数据类型改为 XxxRef 类实例的形式,这样 Lambda 就能持有对它的引用,外部修改和内部修改就能同步了。

为什么捕获变量的上下文变化时需要重新创建 Lambda 对象?

我们来看如下例子:

1
2
3
4
5
6
7
8
fun main() { 
val tasks = mutableListOf<() -> Unit>()
for (i in 1..3) {
val value = i
tasks.add { println(value) } // #1
}
tasks.forEach { it() }
}

上述代码中,#1标记处的 Lambda 表达式就属于捕获变量上下文会变化的情况,因为每一轮循环的 value 变量的值都是不一样的,所以每一轮循环都必须重新创建 Lambda 对象

如果你运行上面的代码,将会输出:

1
2
3
1
2
3

但是如果 Kotlin 不每次都新建一个 Lambda 对象而是复用首次创建的那个对象呢?

在解释这个问题之前我们需要澄清另一个问题——假设 Kotlin 复用首次创建的 Lambda 对象,那么每次遇到相同的变量,但是该变量值改变了,Kotlin 是否应该相应地修改 Lambda 对象内的变量值?

我们这里假定 Kotlin 会进行相应的修改,因为这样才符合逻辑。

那么我们继续分析原来的问题,如果 Kotlin 一直复用首次创建的 Lambda 对象,会发生什么。

value = i = 1 时首次创建 Lambda 对象我们这里将这个对象记为$lambda,其内部有一个成员变量类型为 InfRef,其值为 1

value = i = 2 时,Kotlin 继续复用$lambda,但由于value 的值改变了,所以 Kotlin 也修改$lambda 中的成员变量值为 2

value = i = 3 时与上一种情况同理

那么这个程序最终会输出什么?答案是:

1
2
3
3
3
3

因为被 add 到 tasks 列表中的实际上都是$lambda这同一个对象的引用,并且在最后一次循环结束时,其内部捕获的变量值为 3,所以会输出三个 3,这显然不是我们想要的结果。

那再来看看如果遇到这种情况,Kotlin 每次都新创建一个 Lambda 对象而不是复用。

value = i = 1 时,创建$lambda1,内部变量值设为 1

value = i = 2 时,创建$lambda2,内部变量值设为 2

value = i = 3 时,创建$lambda3,内部变量值设为 3

最终被添加到 tasks 的三个 Lambda 对象都是不同的三个对象,且内部变量的值依次为 1、2、3,最终输出就是 1 2 3.

思考

内存泄漏

从上面的分析可以看出,lambda 表达式一旦捕获了外部变量,那么它就会持有这个变量的引用,那么在 Android 中,如果我们在一个 Activity 中写了一个 lambda 表达式,并且其中使用了这个 Activity 的某些成员变量或方法,那么就会导致这个 Lambda 对象持有本 Activity 的引用,如果这个 Lambda 对象一直没有消除被其他对象持有的话,那么 JVM 的 GC 机制是无法回收 Lambda 对象以及 Activity 的,这就会导致 Activity 泄露,所以在 Android 中使用 lambda 表达式的时候一定要格外小心。

甚至有些时候,仅仅只一个日志打印,就可能造成 Activity 泄露,来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object TaskManager {
var task: (() -> Unit)? = null
}

class MyActivity: AppCompatActivity() {
val TAG = "MyActivity"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 省略...
TaskManager.task = { // #1
Log.d(TAG, "Activity Launched")
}
}
}

TaskManager是一个全局单例,它持有#1 标记处的 Lambda 对象,而这个 Lambda 对象由于内部使用了 TAG,这个 TAG 是 Activity 的成员变量,所以 Lambda 也会持有这个 Activity 对象,只要 TaskManager.task 仍然是这个 Lambda 对象,那么这个 Activity 就永远不会被回收。

解决办法也很简单,把 TAG 改成静态变量即可:

1
2
3
companion object {
const val TAG = "MyActivity"
}

或者把TAG改成顶级变量,MyActivity.kt:

1
2
3
4
const val TAG = "MyActivity"
class MyActivity: AppCompatActivity() {
// ...
}

这样 Lambda 对象就不会持有 Activity 的引用了。

性能优化

在循环体内,若 lambda 表达式捕获外部变量,将导致多个 Lambda 对象被创建,很影响性能

为了避免这种情况,我们可以使用 inline 关键字标记接受 lambda 表达式的函数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
var sum = 0
for (i in 0 until 100) {
process {
sum += i
}
}
}

inline fun process(block: (() -> Unit)) {
println("Processing...")
block()
}

用 inline 标记后的函数,不仅会把函数本身的内容内联到调用处,还会把函数接收的 lambda 表达式直接展开在调用处,上述代码等价于:

1
2
3
4
5
6
7
fun main() {
var sum = 0
for (i in 0 until 100) {
println("Processing...")
sum += i
}
}

这样既没有 Lambda 对象的创建,也没有函数调用的开销,性能得到了提升


Kotlin中的 lambda 表达式
http://47.109.28.82/2026/02/23/Kotlin中的-lambda-表达式/
作者
GraftCopolymer
发布于
2026年2月23日
许可协议