首页 > 解决方案 > 当 contains() 取 E 时,Kotlin 的 Set 如何是协变的?

问题描述

我正在研究几种编程语言的集合库中的协变和逆变,偶然发现了 Kotlin 的Set接口。

它被记录为

interface Set<out E> : Collection<E>

这意味着它是协变的——仅按照 Kotlin 文档“生产” E 对象,而不是使用它们。

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

然而,它有这两种方法:

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

所以当我创建一个实现类时Set<String>,我必须实现(除了其他人)contains(String)。但后来有人可以将我的课程用作 aSet<Any>和 call 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)) 
}

在线运行

所以事实证明这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 有什么特殊的编译器魔法吗?为什么这在任何地方都没有记录?

标签: kotlincollectionscovariance

解决方案


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

在特定情况下Set.contains@UnsafeVariance注释用于确保函数接受 的实例E,因为传递element未传入的实例是E没有contains意义的——所有正确的实现都Set将始终返回false。的实现Set不应该存储element传递给的contains,因此不应该从任何其他具有返回类型的函数返回它E。因此,正确实施Set不会违反运行时的方差限制。

@UnsafeVariance注释实际上抑制了编译器差异冲突,例如在 -position 中使用-projectedout类型参数in

这篇博文最好地描述了它的动机:

@UnsafeVariance注解

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

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

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

@UnsafeVariance因此,我们为此目的引入了类型注解。它被故意延长并突出以警告不要滥用它。

博客文章的其余部分还明确提到containsusing的签名@UnsafeVariance提高了类型安全性。

引入的替代方法@UnsafeVariance是继续contains接受Any,但是此选项缺少对contains调用的类型检查,该类型检查将检测element由于不是E.


推荐阅读