Kotlin suspend 修饰符更改函数签名,但编译器报告重载错误

And*_*lli 6 bytecode suspend kotlin kotlin-coroutines

介绍

给定两个函数,foo()and foo(),第一个是标准的,第二个是可暂停的

fun foo(x: Int): Int {
    return 2*x
}

suspend fun foo(x: Int): Int {
    return 4*x
}
Run Code Online (Sandbox Code Playgroud)

以下代码无法编译,因为具有相同签名的两个函数存在冲突。

冲突重载:public fun foo(x: Int): Int 在文件 t.kt 中定义,public suspend fun foo(x: Int): Int 在文件 t.kt 中定义

暂停功能

如果我对挂起函数的理解是正确的,那么:

  1. 一个Continuation参数被添加到挂起函数中,状态机使用它来停止和启动挂起代码
  2. 挂起标记函数在后台使用的返回类型是Any(因此Object对于 java)

这两个副作用理论上应该足以改变第二个foo()函数签名,因此以不同于第一个的方式看待挂起标记的函数。

分析

起初,我认为函数签名检查可以在实际将代码编译成字节码之前进行。然而,将两个呈现的函数变成实际的字节码实际上会导致 2 个具有 2 个不同签名的方法。

Java:t.kt -> t.decompiled.java

@Metadata(
    mv = {1, 1, 16},
    bv = {1, 0, 3},
    k = 2,
    d1 = {"\u0000\n\n\u0000\n\u0002\u0010\b\n\u0002\b\u0003\u001a\u000e\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u0001\u001a\u0019\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u0001H\u0086@ø\u0001\u0000¢\u0006\u0002\u0010\u0003\u0082\u0002\u0004\n\u0002\b\u0019¨\u0006\u0004"},
    d2 = {"foo", "", "x", "(ILkotlin/coroutines/Continuation;)Ljava/lang/Object;", "app"}
)

public final class TKt {
    public static final int foo(int x) {
        return 2 * x;
    }

    @Nullable
    public static final Object foo(int x, @NotNull Continuation $completion) {
       return Boxing.boxInt(4 * x);
    }
}
Run Code Online (Sandbox Code Playgroud)

KB: 有趣的 foo(x: Int): Int

// access flags 0x19
public final static foo(I)I
   // annotable parameter count: 1 (visible)
   // annotable parameter count: 1 (invisible)
   L0
    LINENUMBER 4 L0
    ICONST_2
    ILOAD 0
    IMUL
    IRETURN
   L1
    LOCALVARIABLE x I L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1
Run Code Online (Sandbox Code Playgroud)

KB: 暂停乐趣 foo(x: Int): Int

  // access flags 0x19
  // signature (ILkotlin/coroutines/Continuation<-Ljava/lang/Integer;>;)Ljava/lang/Object;
  // declaration:  foo(int, kotlin.coroutines.Continuation<? super java.lang.Integer>)
  public final static foo(ILkotlin/coroutines/Continuation;)Ljava/lang/Object; @Lorg/jetbrains/annotations/Nullable;() // invisible
   // annotable parameter count: 2 (visible)
   // annotable parameter count: 2 (invisible)
   @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1
   L0
    LINENUMBER 8 L0
    ICONST_4
    ILOAD 0
    IMUL
    INVOKESTATIC kotlin/coroutines/jvm/internal/Boxing.boxInt (I)Ljava/lang/Integer;
    ARETURN
   L1
    LOCALVARIABLE x I L0 L1 0
    LOCALVARIABLE $completion Lkotlin/coroutines/Continuation; L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2
Run Code Online (Sandbox Code Playgroud)

范围调用

在这一点上,我认为 kotlin 并不总是可以决定调用哪个函数。当然,这两个函数是完全不同且独立的,它们的签名甚至没有部分匹配(不同的返回类型和参数)

重点是在kotlin这个词中,挂起函数只能在协程作用域内部调用,而普通函数在两个地方都可以调用。下表可以作为一个很好的例子,以图形方式分析情况。

+---------------+---------------+-----------------+
|               | Default Scope | Coroutine Scope |
+---------------+---------------+-----------------+
| foo()         | ?             | ?               |
+---------------+---------------+-----------------+
| suspend foo() | ?             | ?               |
+---------------+---------------+-----------------+
Run Code Online (Sandbox Code Playgroud)

可能涉及这两个实体之间的定义冲突的唯一场景如下。

fun foo(x: Int): Int {
    return 2*x
}

suspend fun foo(x: Int): Int {
    return 4*x
}

GlobalScope.launch {
    println(foo(7))
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,如果没有一个假设的(又名 Existing only in my head)运算符让 Kotlin 知道要调用哪个函数,如果是可挂起的函数或标准函数,您就无法确定正在调用哪个函数。

这个分析是正确的还是我错过了介于两者之间的东西?

结论

此问题将在具有类似内容的 YouTrack 问题中链接,这可能是编译器改进的起点(也许区分重载错误与标准函数错误的可挂起冲突),或者对于新的 Kotlin 功能,扩展可挂起函数与普通函数的互操作性(我想象一种类似扩展的运算符,它以函数调用为前缀,并且运算符的存在将一个调用与另一个调用区分开来)。

Man*_*gor 5

您在字节码方面是对的 - 签名是不同的。

然而,它无法从 Kotlin 语言方面明确地确定功能。比如下面应该调用什么方法?

fun main() {
    runBlocking {
        println(foo(1)) // which one should be called here?
    }
}

fun foo(x: Int): Int {
    return 2*x
}

suspend fun foo(x: Int): Int {
    return 4*x
}
Run Code Online (Sandbox Code Playgroud)

Java Synthetic 方法也有相同的行为。请检查此答案- 您可以在字节码中定义两个方法,但是在 Java 语言语法中是不允许的。