kotlin协程代码生成-4.Callable Reference(译)

kotlin协程代码生成翻译文章系列,翻译自 coroutineCodegenUtil.kt

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 标记接口,以支持 isas 挂起函数类型检查。

理想情况下,该对象是单例的,因为它没有内部状态。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。为什么将返回未装箱内联类的挂起函数包装起来会导致双重完成?

当我们逐步跟踪示例的执行时,答案就显而易见了。

  1. returnsIC 函数返回 COROUTINE_SUSPENDED
  2. 可调用引用应该返回装箱类,因为它覆盖了通用的 FunctionN,并且其 invoke 方法返回通用类型。因此,它假设函数返回未装箱的类型并将其装箱。
  3. BaseContinuationImpl.resumeWith 函数没有接收到 COROUTINE_SUSPENDED,因为它被装箱在内联类中,并且运行了关闭过程。
  4. 当我们恢复执行时,挂起函数返回未装箱的内联类。
  5. 可调用引用将其装箱。
  6. BaseContinuationImpl.resumeWith 函数再次运行关闭过程。此时会抛出 KNPE。

为了解决这个问题,对于返回未装箱内联类的挂起函数的可调用引用,会先检查是否为 COROUTINE_SUSPENDED,然后再对返回值进行装箱。

相关推荐
Kapaseker6 小时前
一杯美式深入理解 data class
android·kotlin
alexhilton3 天前
端侧RAG实战指南
android·kotlin·android jetpack
Kapaseker3 天前
2026年,我们还该不该学编程?
android·kotlin
Kapaseker4 天前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
Kapaseker5 天前
一杯美式搞定 Kotlin 空安全
android·kotlin
FunnySaltyFish6 天前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
Kapaseker6 天前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
Kapaseker7 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z9 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton10 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack