kotlin协程代码生成翻译文章系列,翻译自 coroutineCodegenUtil.kt
- kotlin协程代码生成-1.Suspend Lambda(译)
- kotlin协程代码生成-2.Suspend Functions(译)
- kotlin协程代码生成-3.Inline(译)
- kotlin协程代码生成-4.Callable Reference(译)
- kotlin协程代码生成-5.Debug(译)
Callable Reference
考虑以下简单的可调用挂起函数的可调用引用示例:
kotlin
import kotlin.coroutines.*
var c: Continuation<String>? = null
suspend fun callMe() = suspendCoroutine<String> { c = it }
fun builder(c: suspend () -> Unit) {
c.startCoroutine(Continuation(EmptyCoroutineContext) { it.getOrThrow() })
}
suspend fun callSuspend(c: suspend () -> String) = c()
fun main() {
builder {
println(callSuspend(::callMe))
}
c?.resume("OK")
}
我们将可调用引用传递给callSuspend
函数,而不是将 lambda 传递给它。在函数内部,我们调用其 invoke
方法,就好像它是 lambda 一样。 因此,我们需要生成一个带有该方法的对象。但是,与挂起 lambda 不同,该方法始终只调用一个函数。因此它可以是尾调用。由于它是尾调用,我们不能使用 BaseContinuationImpl
作为超类。相反,我们使用 FunctionReferenceImpl
,该类是所有可调用引用的基类。此外,由于该对象重写了 invoke
方法, 该对象实现了 Function{N+1}
接口,其中 N
是挂起函数的 arity。最后,它还应该重写 SuspendFunction
标记接口,以支持 is
和 as
挂起函数类型检查。
理想情况下,该对象是单例的,因为它没有内部状态。JVM_IR BE 就是这样做的:它将所有可调用引用都生成为单例。然而,旧的 BE 不会将可调用引用生成为 挂起函数的单例,这是一个疏忽。尽管如此,这个问题不太可能被解决,因为新的 BE 将来会取代旧的 BE,而且这个疏忽并不足以立即修复它。
最后,旧的 JVM BE 在 invoke
方法中的函数调用周围生成了挂起标记。这是一个 bug,它在 JVM_IR BE 中被修复,但仍然未在旧的 BE 中修复。
Inlining
将可调用的引用内联到挂起函数的操作基本上与将挂起 lambda 内联的操作类似。从内联器的角度来看,可调用的引用到挂起函数就像是一个带有调用的内联 lambda。因此,它应该像一个只有一个调用的挂起 lambda 一样运行。
尚待解决:支持将挂起函数转换为内联函数的可调用引用。否则,甚至简单的版本,例如 something?.let(MyClass::mySuspendMethod)
也会产生错误。
Ordinary -> Suspend conversion(普通到挂起转换)
考虑以下示例
kotlin
import kotlin.coroutines.*
fun callMe(): String = "OK"
var c: Continuation<Unit>? = null
suspend fun suspendMe() = suspendCoroutine<Unit> { c = it }
suspend fun callSuspend(c: suspend () -> String): String {
suspendMe()
return c()
}
fun builder(c: suspend () -> Unit) {
c.startCoroutine(Continuation(EmptyCoroutineContext) { it.getOrThrow() })
}
fun main() {
builder {
println(callSuspend(::callMe))
}
c?.resume(Unit)
}
在这里,我们将一个普通函数的可调用引用传递给一个期望挂起函数类型的函数。因此,我们不能简单地传递可调用引用对象。相反,我们生成一个所谓的适配函数引用。它不应该继承 FunctionReference
,因为适配函数引用不支持反射。因此,这些对象不是继承自 FunctionReferenceImpl
,而是继承自 AdaptedFunctionReference
。
与通常的可调用引用不同,示例中的可调用引用应该接受 continuation 参数,但是它应该忽略它,因为函数是普通的。
Start
与挂起 Lambda 不同,我们无法在从可调用引用开始协程(广义上)时直接调用 create
。因为对象没有 create
方法,也不是一个 continuation。
因此,我们需要手动编写 continuation,并且在 invokeSuspend
函数中也需要手动编写状态机。具体请参见 createCoroutineFromSuspendFunction
。
FIXME:如挂起 Lambda 部分所述,我们可以重用此机制来处理尾调用挂起 Lambda。
Returning Inline Classes
The following example:
kotlin
import kotlin.coroutines.*
inline class IC(val a: Any)
suspend fun callSuspend(c: suspend () -> Any) {
println(c())
}
fun builder(c: suspend () -> Unit) {
c.startCoroutine(Continuation(EmptyCoroutineContext) { it.getOrThrow() })
}
var c: Continuation<IC>? = null
suspend fun returnsIC() = suspendCoroutine<IC> { c = it }
fun main() {
builder {
callSuspend(::returnsIC)
}
c?.resume(IC("OK"))
}
在 1.4.20 版本之前,调用 BaseContinuationImpl.resumeWith
时可能会抛出 KNPE。当我们尝试两次完成协程(广义上)时,就会发生 KNPE。为什么将返回未装箱内联类的挂起函数包装起来会导致双重完成?
当我们逐步跟踪示例的执行时,答案就显而易见了。
returnsIC
函数返回COROUTINE_SUSPENDED
。- 可调用引用应该返回装箱类,因为它覆盖了通用的
FunctionN
,并且其invoke
方法返回通用类型。因此,它假设函数返回未装箱的类型并将其装箱。 BaseContinuationImpl.resumeWith
函数没有接收到COROUTINE_SUSPENDED
,因为它被装箱在内联类中,并且运行了关闭过程。- 当我们恢复执行时,挂起函数返回未装箱的内联类。
- 可调用引用将其装箱。
BaseContinuationImpl.resumeWith
函数再次运行关闭过程。此时会抛出 KNPE。
为了解决这个问题,对于返回未装箱内联类的挂起函数的可调用引用,会先检查是否为 COROUTINE_SUSPENDED
,然后再对返回值进行装箱。