当 contains() 接受 E 时,Kotlin 的 Set 如何是协变的?

2024-04-18

我正在研究几种编程语言的集合库中的协变和逆变,并偶然发现了 Kotlin 的Set https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-set/index.html界面。

它被记录为

interface Set<out E> : Collection<E>

这意味着它是协变的——仅“生成”E 对象,遵循 Kotlin 文档 https://kotlinlang.org/docs/reference/generics.html#declaration-site-variance,不消耗它们。

And Set<String>成为的子类型Set<Any>.

然而,它有这两种方法:

abstract fun contains(element: E): Boolean
abstract fun containsAll(elements: Collection<E>): Boolean

所以当我创建一个类实现Set<String>,我必须实施(除了其他)contains(String)。但后来有人可以用我的课程作为Set<Any>并打电话set.contains(5).

我实际上尝试过这个:

class StringSet : Set<String> {
    override val size = 2
    override fun contains(element: String): Boolean {
        println("--- StringSet.contains($element)")
        return element == "Hallo" || element == "World"
    }

    override fun containsAll(elements: Collection<String>) : Boolean =
        elements.all({it -> contains(it)})
    override fun isEmpty() = false
    override fun iterator() = listOf("Hallo", "World").iterator()

}

fun main() {
    val sset : Set<String> = StringSet()
    println(sset.contains("Hallo"))
    println(sset.contains("xxx"))
    //// compiler error:
    // println(set.contains(5))

    val aset : Set<Any> = sset
    println(aset.contains("Hallo"))
    println(aset.contains("xxx"))
    // this compiles (and returns false), but the method is not actually called
    println(aset.contains(5)) 
}

(在线运行 https://pl.kotl.in/uAKB2qUHH)

事实证明Set<String>不是“真正”的子类型Set<Any>,作为set.contains(5)适用于第二个,但不适用于第一个。

实际上调用 contains 方法甚至可以在运行时工作 - 只是我的实现永远不会被调用,它只是打印false.

查看接口的源代码,发现这两个方法实际上声明为

abstract fun contains(element: @UnsafeVariance E): Boolean
abstract fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean

这里发生了什么? Set 是否有一些特殊的编译器魔法? 为什么这没有任何地方记录?


声明点协方差的形式为out修饰符错过了一个有用的用例,即确保作为参数传递的实例通常在此处传递是合理的。这contains函数就是一个很好的例子。

在特定情况下Set.contains, the @UnsafeVariance注释用于确保该函数接受一个实例E,作为通过element那不是E into contains没有任何意义——所有正确的实施Set总会回来的false。的实施Set不应该存储element传递给contains因此永远不应该从具有返回类型的任何其他函数返回它E。所以一个正确实施的Set不会违反运行时的方差限制。

The @UnsafeVariance注释实际上抑制了编译器方差冲突,就像使用out-投影类型参数in-位置。

其动机最好地描述于这篇博文 https://blog.jetbrains.com/kotlin/2015/10/kotlin-1-0-beta-candidate-is-out/:

@UnsafeVariance注解

有时我们需要在类中抑制声明站点差异检查。例如,要使Set.contains在保持只读集协变的同时保持类型安全,我们必须这样做:

interface Set<out E> : Collection<E> {
     fun contains(element: @UnsafeVariance E): Boolean
}

这给实施者带来了一些责任contains,因为通过抑制此检查,元素的实际类型在运行时可能是任何类型,但有时有必要实现方便的签名。请参阅下面有关集合类型安全的更多信息。

所以,我们介绍了@UnsafeVariance为此目的对类型进行注释。它被故意写得很长,并突出来警告不要滥用它。

The rest of the blog post also explicitly mentions that the signature of contains using @UnsafeVariance improves type-safety.

介绍的替代方案@UnsafeVariance是为了保留contains接受Any,但是这个选项缺少类型检查contains可以检测错误调用的调用element由于不是 的实例而不能出现在集合中E.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

当 contains() 接受 E 时,Kotlin 的 Set 如何是协变的? 的相关文章

随机推荐