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,然后再对返回值进行装箱。

相关推荐
hsx66613 小时前
Kotlin 协程中的 Dispatchers
kotlin
每次的天空17 小时前
Android-重学kotlin(协程源码第二阶段)新学习总结
android·学习·kotlin
stevenzqzq17 小时前
Kotlin 中主构造函数和次构造函数的区别
android·kotlin
开发者如是说20 小时前
言叶是如何对文件进行端到端加密的
android·kotlin·swift
小李飞飞砖20 小时前
kotlin
开发语言·单例模式·kotlin
小李飞飞砖20 小时前
kotlin中的冷流和热流
android·开发语言·kotlin
Kotlin上海用户组2 天前
Koin vs. Hilt——最流行的 Android DI 框架全方位对比
android·架构·kotlin
Kapaseker2 天前
当Object遇到Json你可能会碰到的坑
kotlin
RichardLai882 天前
Kotlin Flow:构建响应式流的现代 Kotlin 之道
android·前端·kotlin
程序员江同学2 天前
Kotlin/Native 编译流程浅析
android·kotlin