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

相关推荐
居居飒13 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
刘争Stanley2 天前
如何高效调试复杂布局?Layout Inspector 的 Toggle Deep Inspect 完全解析
android·kotlin·android 15·黑屏闪屏白屏
sickworm陈浩2 天前
Java 转 Kotlin 系列:究竟该不该用 lateinit?
android·kotlin
droidHZ3 天前
Compose Multiplatform 之旅—声明式UI
android·kotlin
zhangphil3 天前
Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆角矩形实现,Kotlin(1)
android·kotlin
alexhilton5 天前
Android技巧:学习使用GridLayout
android·kotlin·android jetpack
zhangphil6 天前
Android使用PorterDuffXfermode的模式PorterDuff.Mode.SRC_OUT实现橡皮擦,Kotlin(1)
android·kotlin
IH_LZH7 天前
OkHttp源码分析:分发器任务调配,拦截器责任链设计,连接池socket复用
android·java·okhttp·kotlin
casual_clover8 天前
Android之RecyclerView显示数据列表和网格
android·kotlin
氤氲息9 天前
导入kotlin
android·开发语言·kotlin