擦除在 Kotlin 中是如何工作的?

Posted

技术标签:

【中文标题】擦除在 Kotlin 中是如何工作的?【英文标题】:How does erasure work in Kotlin? 【发布时间】:2017-08-12 12:14:53 【问题描述】:

在 Kotlin 中,以下代码编译:

class Foo 
    fun bar(foo: List<String>): String 
        return ""
    

    fun bar(foo: List<Int>): Int 
        return 2;
    

然而,这段代码没有:

class Foo 
    fun bar(foo: List<String>): String 
        return ""
    

    fun bar(foo: List<Int>): String 
        return "2";
    

编译这个会导致如下错误:

Error:(8, 5) Kotlin: Platform declaration ***: The following declarations have the same JVM signature (foo(Ljava/util/List;)Ljava/lang/String;):
    fun foo(layout: List<Int>): String
    fun foo(layout: List<String>): String

在 Java 中,这两个示例都不会编译:

class Foo 
    String bar(List<Integer> foo) 
        return "";
    

    Integer bar(List<String> foo) 
        return 2;
    


class Foo 
    String bar(List<Integer> foo) 
        return "";
    

    String bar(List<String> foo) 
        return "2";
    

不出所料,前面的两个 sn-ps 都会产生熟悉的编译器错误:

Error:(13, 12) java: name ***: bar(java.util.List<java.lang.String>) and bar(java.util.List<java.lang.Integer>) have the same erasure

令我惊讶的是,第一个 Kotlin 示例完全有效,其次,如果有效,为什么第二个 Kotlin 示例会失败? Kotlin 是否将方法的返回类型视为其签名的一部分?此外,与 Java 相比,为什么 Kotlin 中的方法签名尊重完整的参数类型?

【问题讨论】:

除了其他答案之外,返回类型不会添加到 Java 中的方法签名中。 【参考方案1】:

实际上 Kotlin 知道您的示例中这两种方法之间的区别,但 jvm 不会。这就是为什么它是“平台”冲突。

您可以使用@JvmName 注解编译您的第二个示例:

class Foo 
  @JvmName("barString") fun bar(foo: List<String>): String 
    return ""
  

  @JvmName("barInt") fun bar(foo: List<Int>): String 
    return "2";
  

此注释正是出于这个原因而存在。你可以在interop documentation阅读更多内容。

【讨论】:

但是为什么第一个 Kotlin 示例编译时没有这个注解呢? JVM 是否识别具有不同返回类型的方法?根据this answer,返回类型不是方法签名的一部分。 啊,我明白了。也许Java在编译期间不允许具有不同返回类型的相同名称/参数方法,但是JVM可以识别它们? @breandan 是的,返回类型是签名的一部分,但在第一种情况下,这两种方法不同,(即使参数擦除类型相同)所以签名是不同的。它们分别是foo(Ljava/util/List;)Ljava/lang/String;foo(Ljava/util/List;)Ljava/lang/Integer;。 Java 在名称冲突检查中仅考虑参数类型和方法名称,因此 Java 中的第一个示例未编译。这更多是关于重载和类型擦除,并且可以在语言中轻松修复(就像 Kotlin 所做的那样) 我不认为 Kotlin 将方法的返回类型视为其类型签名的一部分。例如,它不允许在同一个类中定义两个方法 @JvmName("barInt") fun bar(foo: List&lt;String&gt;): Int@JvmName("barString") fun bar(foo: List&lt;String&gt;): String,即使它们的 JVM 签名不同(分别为 barInt(Ljava/util/List;)Ljava/lang/Integer;barString(Ljava/util/List;)Ljava/lang/String;)。但是,它将允许@JvmName("barInt") fun bar(foo: List&lt;Int&gt;): Int@JvmName("barString") fun bar(foo: List&lt;String&gt;): String 在同一个类中。 这是因为重载决议永远不能选择这些方法中的一个而不是另一个(它不考虑返回类型),所以这两个方法都不能被调用。【参考方案2】:

虽然@Streloks 的回答是正确的,但我想更深入地了解它的工作原理。

第一个变体起作用的原因是它在 Java 字节代码中没有被禁止。虽然 Java 编译器会抱怨它,即 Java language specification does not allow it,但 Byte 代码会抱怨,正如 https://community.oracle.com/docs/DOC-983207 和 https://www.infoq.com/articles/Java-Bytecode-Bending-the-Rules 中所记录的那样。在 Byte 代码中,每个方法调用都是指方法的实际返回类型,而在编写代码时并非如此。

很遗憾,我找不到真正的来源,为什么会这样。

关于Kotlins name resolution的文档包含一些有趣的点,但我没有看到你的实际案例。

真正帮助我理解的是answer from @Yole 到Kotlin type erasure - why are functions differing only in generic type compilable while those only differing in return type are not?,更准确地说,kotlin 编译器在决定调用哪个方法时不会考虑变量的类型。

因此,在变量上指定类型不会影响要调用的方法,而是相反,即被调用的方法(有或没有泛型信息)会影响类型,这是一个深思熟虑的设计决定要使用的。

将规则应用于以下示例是有意义的:

fun bar(foo: List<String>) = ""    (1)
fun bar(foo: List<Int>) = 2        (2)

val x = bar(listOf("")) --> uses (1), type of x becomes String
val y = bar(listOf(2))  --> uses (2), type of y becomes Int

或者有一个提供泛型类型但甚至不使用它的方法:

fun bar(foo: List<*>) = ""         (3)
fun <T> bar(foo: List<*>) = 2      (4)

val x = bar(listOf(null))          --> uses (3) as no generic type was specified when calling the method, type of x becomes String
val y = bar<String>(listOf(null))  --> uses (4) as the generic type was specified, type of y becomes Int

这也是为什么以下方法不起作用的原因:

fun bar(foo: List<*>) = ""
fun bar(foo: List<*>) = 2

这是不可编译的,因为它会导致冲突重载,因为在尝试识别要调用的方法时未考虑分配变量本身的类型:

val x : String = bar(listOf(null)) // ambiguous, type of x is not relevant

现在关于名称冲突:只要您使用相同的名称、相同的返回类型和相同的参数(其泛型类型被删除),您实际上将在字节码中获得完全相同的方法签名。这就是为什么@JvmName 变得必要的原因。这样,您实际上可以确保字节码中没有名称冲突。

【讨论】:

您是说在 kotlin 中允许仅通过返回类型重载方法吗?因为如果你这样做,fun go(): String = ""; fun go(): Int = 12; 必须编译,它不会 不,我不想这么说...我的意思是“第一个示例中显示的星座”和“允许”...我将重新表述那个... . Kotlin 确实有一种特殊的方法来处理泛型...我不明白为什么以下工作:fun go() : String = ""; fun &lt;T&gt; go() : Int = 12...我的意思是...我什至没有在第二个中使用T...另一方面......如果这是有效的,为什么你提供的样本不能工作? &lt;T&gt; 的示例是我正在研究的问题... :) 修改了我的答案......让我知道现在是否更清楚 我对此没有任何解释,因此正在研究这个问题......今天可能会发布它

以上是关于擦除在 Kotlin 中是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 中的 reified 关键字是如何工作的?

Kotlin在Android中的发展趋势

如何在 Kotlin 中检查泛型类型

“.()”在 Kotlin 中是啥意思?

Kotlin学习手记--泛型泛型约束泛型型变星投影泛型擦除内联特化

Kotlin | 浅谈 reified 与泛型 那些事