当contains()取E时,Kotlin的Set如何协变?

时间:2019-05-22 18:00:29

标签: kotlin collections covariance

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

记录为

interface Set<out E> : Collection<E>

这意味着它是协变的–仅“产生” E个对象following the Kotlin documentation,而不消耗它们。

然后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)) 
}

Run online

因此,事实证明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有一些特殊的编译器魔术吗? 为什么在任何地方都没有记录?

1 个答案:

答案 0 :(得分:5)

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

Set.contains的特定情况下,@UnsafeVariance批注用于确保函数接受E的实例,并传递不是{将{1}}放入element毫无意义-E的所有正确实现都将始终返回containsSet的实现不应该存储传递给false的{​​{1}},因此永远不要从其他任何具有返回类型Set的函数中返回它。因此,正确实施的element不会在运行时违反差异限制。

contains注释实际上可以抑制编译器方差冲突,就像在E位置使用Set投影类型参数一样。

最好在this blog post中描述其动机:

  

@UnsafeVariance批注

     

有时,我们需要在类中禁止声明站点的方差检查。例如,要使out具有类型安全性,同时保持只读集为协变,我们必须这样做:

in
     

这给@UnsafeVariance的实现者带来了一些责任,因为在执行此检查的情况下,取消了元素的实际类型可能在运行时完全是任何东西,但有时必须获得方便的签名。请在下面查看有关集合类型安全性的更多信息。

     

因此,我们为此目的在类型上引入了Set.contains批注。故意将其设置成较长的长度,并脱颖而出以警告再次滥用它。

博客的其余部分还明确提到使用interface Set<out E> : Collection<E> { fun contains(element: @UnsafeVariance E): Boolean } contains进行签名可以提高类型安全性。

引入@UnsafeVariance的替代方法是使contains接受@UnsafeVariance,但是此选项缺少对@UnsafeVariance呼叫的类型检查,该检查不会检测到{{1 }}由于不是contains的实例而不能出现在集合中。