在kotlin协程中将非挂起函数类型赋值给挂起函数类型

5 阅读2分钟

在kotlin协程中将非挂起函数类型赋值给挂起函数类型

问题

private fun fetchData(): String {
    println("fetchData")
    return "this is data!"
}

private fun invokeFetchImageCase() {
    // completion
    val completion = object : Continuation<String> {
        override val context: CoroutineContext = EmptyCoroutineContext
        override fun resumeWith(result: Result<String>) {
            println("completion2 result ${result.getOrThrow()}")
        }
    }

    // 将::fetchImage这个非挂起函数类型赋值给挂起函数类型
    // 本来是Function0,编译器偷偷做了工作,将他变成了Function1
    val block: suspend () -> String = ::fetchImage
    
    // 并且可以强转为Function1<Continuation<String>, Any?>(粗略)
    // 调用invoke(it)以后fetchImage函数本身的方法体会被执行,然后返回结果
    // 就好像在调用一个带挂起关键字但是永远不会挂起的方法
    val result = (block as Function1<Continuation<String>, Any?>).invoke(completion)

    println("asInvokeCase result $result")
}
private fun fetchData(): String {
    println("fetchData")
    return "this is data!"
}

val block: suspend () -> String = ::fetchImage

注意“::fetchImage”的类型为"() -> String",但是kotlin语法却允许将这个非挂起函数类型赋值给"suspend () -> String"

通过block::class.java查看block运行时类型

// block::class.java.superclass
block运行时类型 -> AdaptedFunctionReference -> Object

// block::class.java.interfaces
Function1
SuspendFunction

// block::class.java.interface
invoke(java.lang.Object)
invoke(kotlin.coroutines.Continuation)
..(其他若干方法)

可以看到block运行时类型实现了Function1,然后多了2个invoke()方法,其中invoke(java.lang.Object)显然是由于泛型导致的桥接方法。

那么在运行时,fetchData中的代码如何被执行起来呢?

执行block

如果想让fetchData()函数的函数体被执行,只需调用block执行的运行时类型的invoke()方法即可:

  1. 方式1:在挂起方法中调用block()
val block: suspend () -> String = ::fetchData
runBlocking {
    // 反编译以后可以看到实际会调用
    // block.invoke(当前状态机)
    block()
}

// 输出:
// fetchData
// asInvokeCase result this is data!
  1. 方式2:通过as关键字强转为Function1,然后调用invoke(continuation)
val block: suspend () -> String = ::fetchData

val completion = completion<String> {
    println("completion2 result $it")
}

// 强转为Function1
val function1 = block as Function1<Continuation<String>, Any?>
// 调用invoke
val result = function1.invoke(completion)

println("asInvokeCase result $result")

// 输出:
// fetchData
// asInvokeCase result this is data!

观察输出结果发现:

  1. fetchData的函数体被调用了
  2. 传递给invoke方法的completion不会被调用

另外,强转类型“Function1<Continuation, Any?>”是参考kotlin协程源码得来的:

// IntrinsicsJvm.kt
public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
): Continuation<Unit> {
    val probeCompletion = probeCoroutineCreated(completion)
    return if (this is BaseContinuationImpl)
        create(probeCompletion)
    else
        createCoroutineFromSuspendFunction(probeCompletion) {
            // 参考的这里
            (this as Function1<Continuation<T>, Any?>).invoke(it)
        }
}

block运行时类型伪代码

结合block::class.java与上面的测试代码,block运行时类型伪代码如下:

Function1<Continuation<String>, Object> block = new Function1<Continuation<String>, Object>() {

    @Override
    public Object invoke(Continuation<String> continuation) {
        return fetchData();
    }
    
};

编译器相当于至少干了如下事情:

  1. 让这个类型实现了Function1<Continuation, Object>
  2. invoke()方法内部直接调用fetchData()然后直接返回,并不会回调continuation

为什么kotlin中运行这种赋值?

让非挂起函数可以直接被当成挂起函数使用,减少模版代码

其他例子

val b = {
    println("run!")
    "100"
}

// b::class.java.interfaces
// Function0
kele(b)

private fun kele(block: suspend () -> String) {
    // block::class.java.interfaces
    // Function1
    // SuspendFunction

    //神奇,block和外面的b不一样了

    val completion = object : Continuation<String> {
        override val context: CoroutineContext = EmptyCoroutineContext
        override fun resumeWith(result: Result<String>) {
            println("completion 回调:${result.getOrThrow()}") // 挂起恢复后触发
        }
    }

    block.startCoroutineUninterceptedOrReturn(completion)
}

注意:block和外面的b不一样了!