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

huangapple go评论62阅读模式
英文:

Kotlin suspend modifier changes function signature, but compiler reports overload error

问题

介绍

给定两个函数,foo()foo(),第一个是标准函数,第二个是可挂起函数

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

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

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

冲突的重载:在文件 t.kt 中定义的公共 fun foo(x: Int): Int 与在文件 t.kt 中定义的公共 suspend fun foo(x: Int): Int 冲突

可挂起函数

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

  1. 可挂起函数会添加一个 Continuation 参数,由状态机用于停止和启动可挂起代码。
  2. 在幕后用于标记为挂起的函数的返回类型是 Any(因此对于 Java 是 Object)。

理论上,这两个副作用足以改变第二个 foo() 函数的签名,因此从第一个函数中不同的视角来看待标记为挂起的函数。

分析

起初,我认为函数签名检查可能是在实际将代码编译成字节码之前执行的。
然而,将这两个呈现的函数转换为实际的字节码后,实际上会得到具有 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@₆\u0002\u0000¢\u0006\u0002\u0010\u0003¢\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);
    }
}

KB:fun 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

KB:suspend fun 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

作用域调用

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

关键在于在 Kotlin 中,挂起函数只能从协程作用域内调用,但普通函数可以从两个地方调用。
以下表格可以作为一个很好的示例来图形化分析情况。

+---------------+---------------+-----------------+
|               | 默认作用域      | 协程作用域       |
+---------------+---------------+-----------------+
| foo()         | ✓             | ✓               |
+---------------+---------------+-----------------+
| suspend foo() | ✘             | ✓               |
+---------------+---------------+-----------------+

唯一可能涉及这两个实体之间的定义冲突的情况是以下情况。

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

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

GlobalScope.launch {
    println(foo(7))
}

在这种情况下,如果没有一个假设的运算符(即只存在于我的头脑中),让 Kotlin 知道调用哪个函数,无论是可挂起的还是标准的,都不能确定调用哪个函数。

这个分析是正确的,还是我中间漏掉了什么东西?

结论

此问题将链接到一个具有类似内容的 YouTrack 问题,这可能是编译器改进的起点(也许是将挂起函数与普通函数的重载错误区分开的一种方式),或者是一个新的 Kotlin 功能,扩展挂起函数与普通函数的互操作性(我正在想象一种类似于

英文:

Introduction

Given two functions, foo() and foo(), the first one is standard and the second is suspendible

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

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

The following code does not compile, because two functions with the same signature are conflicting.

> conflicting overloads: public fun foo(x: Int): Int defined in file t.kt, public suspend fun foo(x: Int): Int defined in file t.kt

Suspending Functions

If my understanding of suspending function is correct, then:

  1. A Continuation parameter is added to a suspending function, used by the state machine to stop and start the suspending code
  2. The return type used under the hood for suspend-marked functions is Any (Thus Object for java)

Those two side effects in theory should be enough to alter the second foo() function signature, hence to view the suspend-marked functions differently from the first one.

Analysis

At first, I have supposed that the function signature check may be performed before actually compiling the code into bytecode.
However, having the two presented functions turned into actual bytecode actually results in 2 methods having 2 different signatures.

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

@Metadata(
    mv = {1, 1, 16},
    bv = {1, 0, 3},
    k = 2,
    d1 = {&quot;\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@&#248;\u0001\u0000&#162;\u0006\u0002\u0010\u0003\u0082\u0002\u0004\n\u0002\b\u0019&#168;\u0006\u0004&quot;},
    d2 = {&quot;foo&quot;, &quot;&quot;, &quot;x&quot;, &quot;(ILkotlin/coroutines/Continuation;)Ljava/lang/Object;&quot;, &quot;app&quot;}
)

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);
    }
}

KB: fun 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

KB: suspend fun foo(x: Int): Int

  // access flags 0x19
  // signature (ILkotlin/coroutines/Continuation&lt;-Ljava/lang/Integer;&gt;;)Ljava/lang/Object;
  // declaration:  foo(int, kotlin.coroutines.Continuation&lt;? super java.lang.Integer&gt;)
  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

Scoped Calls

At this point I thought that it is not always possible for kotlin to decide which function to call.
Sure, these two functions are completely different and separate, their signature has not even a partial match (Different return type and arguments)

The point is that in the kotlin word, the suspending function can only be called from inside a coroutine scope, but the normal function can be called from both places.
The following table can serve as a great example to graphically analyse the situation.

+---------------+---------------+-----------------+
|               | Default Scope | Coroutine Scope |
+---------------+---------------+-----------------+
| foo()         | ✓             | ✓               |
+---------------+---------------+-----------------+
| suspend foo() | ✘             | ✓               |
+---------------+---------------+-----------------+

The only scenario that may involve a definition collision between these two entities is the following.

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

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

GlobalScope.launch {
    println(foo(7))
}

In this case, without an hypothetical (a.k.a. Existing only in my head) operator letting Kotlin know which function to invoke, if the suspendible one or the standard one, you can't be sure about which function you are invoking.

Is this analysis correct or am i missing something in between?

Conclusion

This question will be linked in a YouTrack issue with a similar content, and this may be the starting point for a compiler improvement (Maybe differentiating overload errors from suspendible clashing with standard function error), or for a new Kotlin feature, expanding the suspendible functions interoperability with normal functions (I'm imagining a sort of spread-like operator which is prefixed to the function call, and the presence of the operator differentiates one call from another).

答案1

得分: 7

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

然而,从 Kotlin 语言的角度来看,无法明确确定要调用哪个函数。例如,下面的代码应该调用哪个方法?

fun main() {
    runBlocking {
        println(foo(1)) // 这里应该调用哪一个?
    }
}

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

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

与 Java 合成方法的情况相同。请查看 此答案 - 虽然可以在字节码中定义两个方法,但在Java语言语法中是不允许的。

英文:

You are right in regards of bytecode - the signatures are different.

However it is unable to determine function unambiguously from Kotlin language side. For example, what method should be called below?

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
}

The same behavior is with Java Synthetic methods. Please check this answer - you can define two methods in bytecode, however it isn't allowed in Java Language syntax.

huangapple
  • 本文由 发表于 2020年1月3日 21:09:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/59579182.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定