Kotlin 从 generateSequence() 到 flow() ,但是生成了错误的字节码

Kotlin from generateSequence() to flow() , but wrong bytecode generated

我有一个使用 http 请求检索远程页面的函数。该页面可能有一个或零个 next 页面的 link。如果我想生成所有页面的链,generateSequence 是一个理想的解决方案。这就是我所做的:

首先,有两个效用函数:

fun getBlockingDocument(url: String): Document? 顾名思义,就是一个阻塞函数。该实现只是发送 HTTP 请求并解析为 JSoup 文档。

fun getNextIndexPage(doc: Document, url: String): String?,也是屏蔽功能,不过跟网络无关,只是解析获取下一页,所以这里屏蔽是可以的。

好的,这是序列代码:

val initUrl = // initial url

generateSequence(tools.getBlockingDocument(initUrl).let { initUrl to it }) { (url, doc) ->
  doc?.let {
    parser.getNextIndexPage(doc, url)
  }?.let { nextUrl ->
    nextIndexUrl to tools.getBlockingDocument(nextUrl)
  }
}.forEachIndexed { index, urlAndDoc ->
  val url = urlAndDoc.first
  logger.info("[{}] : {}", index, url)
}

它运行良好,并正确链接了所有页面。

但是如果我将网络调用更改为挂起函数怎么办?这是我创建的:

suspend fun getSuspendingDocument(url: String): Document?

我没有发现与 flow 相似的 generateSequence 构建器样本,所以我这样实现:

  @ExperimentalCoroutinesApi
  @Test
  fun testGetAllPagesByFlow() {
    val flow = flow<Pair<String, Document?>> {
      suspend fun generate(url: String) {
        tools.getSuspendingDocument(url)?.let { url to it }?.also { (url, doc) ->
          emit(url to doc)

          parser.getNextIndexPage(doc, url)?.also { nextUrl ->
            generate(nextUrl) // recursive
          }
        }
      }
      generate("http://...initial url here")
    } // flow

    runBlocking {
      flow.collectIndexed { index, urlAndDoc ->
        val url = urlAndDoc.first
        logger.info("[{}] : {}", index, url)
      }
    }
  }

我使用递归调用 (fun generate()) 来 emit 在每个页面中找到的下一个 url。我不确定这是否是创建 Flow 的惯用方法,但我没有发现类似的代码。如果你有better/idiomatic方法,请告诉我,非常感谢!

无论如何,我认为它应该工作,但我的 IDE (IntelliJ) 抱怨 wrong bytecode generated,我以前从未见过它。

Error:Kotlin: [Internal Error] org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error: wrong bytecode generated
  @Lorg/jetbrains/annotations/Nullable;() // invisible
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
   L1
    ALOAD 0
    GETFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow.p$ : Lkotlinx/coroutines/flow/FlowCollector;
    ASTORE 2
   L2
   L3
    LINENUMBER 44 L3
    NEW destiny/data/FlowTest$testGetAllPagesByFlow$flow
    DUP
    ALOAD 2
    ALOAD 3
    ACONST_NULL
    INVOKESPECIAL destiny/data/FlowTest$testGetAllPagesByFlow$flow.<init> (Lkotlinx/coroutines/flow/FlowCollector;Ldestiny/data/FlowTest$testGetAllPagesByFlow$flow;Lkotlin/coroutines/Continuation;)V
    ASTORE 3
   L4
   L5
    LINENUMBER 56 L5
    ALOAD 3
    CHECKCAST destiny/data/FlowTest$testGetAllPagesByFlow$flow
    ALOAD 0
    GETFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow.$initUrl : Ljava/lang/String;
    ALOAD 0
    ALOAD 0
    ALOAD 2
    PUTFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow.L[=13=] : Ljava/lang/Object;
    ALOAD 0
    ALOAD 3
    PUTFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow.L : Ljava/lang/Object;
    ALOAD 0
    ICONST_1
    PUTFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow.label : I
    INVOKEVIRTUAL destiny/data/FlowTest$testGetAllPagesByFlow$flow.invoke (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
   L6
    DUP
    ALOAD 4
    IF_ACMPNE L7
   L8
    LINENUMBER 42 L8
    ALOAD 4
    ARETURN
   L9
    ALOAD 0
    GETFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow.L : Ljava/lang/Object;
    CHECKCAST destiny/data/FlowTest$testGetAllPagesByFlow$flow
    ASTORE 3
    ALOAD 0
    GETFIELD destiny/data/FlowTest$testGetAllPagesByFlow$flow.L[=13=] : Ljava/lang/Object;
    CHECKCAST kotlinx/coroutines/flow/FlowCollector
    ASTORE 2
   L10
    ALOAD 1
    INVOKESTATIC kotlin/ResultKt.throwOnFailure (Ljava/lang/Object;)V
    ALOAD 1
   L7
    LINENUMBER 58 L7
    POP
   L11
   L12
    GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;
    ARETURN
   L13
   L14
   L15
    NEW java/lang/IllegalStateException
    DUP
    LDC "call to 'resume' before 'invoke' with coroutine"
    INVOKESPECIAL java/lang/IllegalStateException.<init> (Ljava/lang/String;)V
    ATHROW
    RETURN
   L16
    LOCALVARIABLE $this$flow Lkotlinx/coroutines/flow/FlowCollector; L2 L14 2
    LOCALVARIABLE $fun$generate Ldestiny/data/FlowTest$testGetAllPagesByFlow$flow; L4 L11 3
    LOCALVARIABLE this Ldestiny/data/FlowTest$testGetAllPagesByFlow$flow; L0 L13 0
    LOCALVARIABLE $result Ljava/lang/Object; L0 L13 1
    MAXSTACK = 5
    MAXLOCALS = 4
File being compiled at position: (42,46) in /destiny/data/core/src/test/java/destiny/data/FlowTest.kt
The root cause org.jetbrains.kotlin.codegen.CompilationException was thrown at: org.jetbrains.kotlin.codegen.TransformationMethodVisitor.visitEnd(TransformationMethodVisitor.kt:92)
    at org.jetbrains.kotlin.codegen.FunctionCodegen.endVisit(FunctionCodegen.java:990)
    at org.jetbrains.kotlin.codegen.FunctionCodegen.generateMethodBody(FunctionCodegen.java:487)
    at org.jetbrains.kotlin.codegen.FunctionCodegen.generateMethod(FunctionCodegen.java:260)
    at org.jetbrains.kotlin.codegen.FunctionCodegen.generateMethod(FunctionCodegen.java:176)
    ...

太长了,剩下的就省略了

这段代码有什么问题?

如果递归方式不理想,有没有更好的解决方案? (像generateSequence,很漂亮)。谢谢。

环境:

<kotlin.version>1.3.50</kotlin.version>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
<dependency>
  <groupId>org.jetbrains.kotlinx</groupId>
  <artifactId>kotlinx-coroutines-core</artifactId>
  <version>1.3.2</version>
</dependency>
IntelliJ 2018.3.6

$ java -version
java version "11.0.3" 2019-04-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.3+12-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.3+12-LTS, mixed mode)

为什么要使用递归?下面的代码会做同样的事情

val f: Flow<Pair<String, Document?>> = flow {
    var nextUrl: String? = url

    while (nextUrl != null) {
        val doc = tools.getSuspendingDocument(nextUrl)
        emit(url to doc)

        if (doc == null) break;

        nextUrl = parser.getNextIndexPage(doc, url)

    }
}