升级至Kotlin 2.0后出现“Leaked in-place lambda”警告问题排查
以下是重载Reader.useLines()的Kotlin代码:useLinesA直接转发原方法调用,useLinesB先过滤序列再传递给消费者。该代码在Kotlin 1.9可正常编译,但升级至2.0后出现**"Leaked in-place lambda"警告**,同时合约检查提示**"指定EXACTLY_ONCE但实际调用次数为ZERO"**。
代码示例
import java.io.Reader import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind.EXACTLY_ONCE import kotlin.contracts.contract private val prefixes: Array<out String> = arrayOf( "foo", "bar", "baz", ) private fun String.hasPrefix(): Boolean = prefixes.any { prefix -> this.startsWith(prefix) } @OptIn(ExperimentalContracts::class) fun <T> Reader.useLinesA( block: (Sequence<String>) -> T, ): T { contract { callsInPlace(block, EXACTLY_ONCE) } return block.let(this::useLines) } @OptIn(ExperimentalContracts::class) fun <T> Reader.useLinesB( block: (Sequence<String>) -> T, ): T { contract { callsInPlace(block, EXACTLY_ONCE) } return { lines: Sequence<String> -> lines.filterNot(String::hasPrefix).let(block) }.let(this::useLines) }
警告与错误信息
Lambda泄漏警告
fun <T> Reader.useLinesA( block: (Sequence<String>) -> T, ): T { contract { // Leaked in-place lambda 'block: (Sequence<String>) -> T'. callsInPlace(block, EXACTLY_ONCE) } return block.let(this::useLines) // Leaked in-place lambda 'block: (Sequence<String>) -> T'. } fun <T> Reader.useLinesB( block: (Sequence<String>) -> T, ): T { contract { // Leaked in-place lambda 'block: (Sequence<String>) -> T'. callsInPlace(block, EXACTLY_ONCE) } return { lines: Sequence<String> -> lines.filterNot(String::hasPrefix).let(block) // Leaked in-place lambda 'block: (Sequence<String>) -> T'. }.let(this::useLines) }
合约校验错误
Wrong invocation kind 'EXACTLY_ONCE' for 'block: (Sequence<String>) -> T' specified, the actual invocation kind is 'ZERO'.
1. 代码存在的核心问题
代码错误地为传入的block lambda添加了callsInPlace(EXACTLY_ONCE)合约声明,但实际逻辑是将block传递给Reader.useLines执行——这违反了Kotlin 2.0对callsInPlace合约的严格约束:被标记为callsInPlace的lambda必须在当前函数的调用栈内直接执行,不能被传递到其他函数中执行。Kotlin 1.9的合约检查没有这么严格,所以之前能通过,但2.0强化了静态分析,直接触发警告和错误。
2. "Leaked in-place lambda"的原因及修复方案
原因
这里的"Leaked"并非传统意义上的内存泄漏,而是Kotlin合约检查中的术语:指被标记为callsInPlace的lambda被传递到了当前函数作用域之外(这里是传递给Reader.useLines),当前函数无法保证该lambda的执行时机和上下文符合callsInPlace的约定(比如必须在当前栈内执行一次)。
修复方案
直接移除自定义的contract块即可:
Reader.useLines本身已经带有正确的callsInPlace合约声明,不需要重复添加;- 我们的封装函数只是转发或包装lambda,并没有直接执行
block,没有资格声明callsInPlace合约。
修复后的代码示例:
import java.io.Reader private val prefixes: Array<out String> = arrayOf( "foo", "bar", "baz", ) private fun String.hasPrefix(): Boolean = prefixes.any { prefix -> this.startsWith(prefix) } fun <T> Reader.useLinesA( block: (Sequence<String>) -> T, ): T { return block.let(this::useLines) } fun <T> Reader.useLinesB( block: (Sequence<String>) -> T, ): T { return { lines: Sequence<String> -> lines.filterNot(String::hasPrefix).let(block) }.let(this::useLines) }
3. 为何实际调用block一次却提示ZERO次
Kotlin的合约静态分析器只能追踪当前函数内部的代码逻辑,无法深入到其他函数(比如Reader.useLines)的实现中。我们的自定义函数里没有直接调用block,只是把它传递给了useLines,所以分析器认为当前函数内block的调用次数是ZERO,但我们的合约声明了EXACTLY_ONCE,两者冲突,因此触发错误提示。
内容的提问来源于stack exchange,提问作者Андрей Щеглов

