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

相关推荐
小白学大数据2 小时前
正则表达式在Kotlin中的应用:提取图片链接
开发语言·python·selenium·正则表达式·kotlin
bytebeats2 天前
Kotlin 注解全面指北
android·java·kotlin
jzlhll1232 天前
kotlin android Handler removeCallbacks runnable不生效的一种可能
android·开发语言·kotlin
&岁月不待人&2 天前
Kotlin 协程使用及其详解
开发语言·kotlin
苏柘_level62 天前
【Kotlin】 基础语法笔记
开发语言·笔记·kotlin
大福是小强2 天前
002-Kotlin界面开发之Kotlin旋风之旅
kotlin·函数式编程·lambda·语法·运算符重载·扩展函数
大耳猫2 天前
Android Studio 多工程公用module引用
android·java·kotlin·android studio
良技漫谈2 天前
Rust移动开发:Rust在Android端集成使用介绍
android·程序人生·rust·kotlin·学习方法
北欧人写代码2 天前
idea java 项目右键new file时 为什么是 kotlin class 不是普通class
java·kotlin·intellij-idea
zhangphil3 天前
Android LoaderManager AsyncTaskLoader,Kotlin(4)
android·kotlin