为什么函数参数中的反类型参数在“出”位置被考虑?

时间:2019-05-02 23:20:53

标签: generics kotlin contravariance

我很难用英语来描述,但这是问题所在:

class Consumer<in T> {
    fun consume(t: T) {}
}

class Accepter<in T>() {
    // ERROR: Type parameter T is declared as 'in' but occurs in 'out' position in type Consumer<T>
    fun acceptWith(value: T, consumer: Consumer<T>) {}
}

可以这样修复:

fun <U : T> acceptWith(value: T, consumer: Consumer<U>) {}

但是我不明白这个问题。允许Consumer<T>似乎并不安全。有人可以解释吗?

2 个答案:

答案 0 :(得分:2)

自变量位置称为逆变,因为其方差沿相反的方向w.r.t。类差异。这意味着类的超类型可以将参数类型的子类型作为参数,反之亦然。

让我们考虑一些实际的参数类型S。在此示例中,类型Accepter<S>Accepter<Any>的超类型,必须使用Consumer<Any>子类型作为参数,但是使用给定的签名,它将Consumer<S>,不是Consumer<Any>的子类型,而是它的超类型。

另一个示例说明为什么允许该参数类型不安全。让我们考虑以下AccepterConsumer的实现:

class AnyAccepter : Accepter<Any>() {
    override fun acceptWith(value: Any, consumer: Consumer<Any>) {
        consumer.consume(Any())
    }
}

class StringConsumer : Consumer<String>() {
    override fun consume(t: String) {
        println(t.length)
    }
}
fun main() {
    val anyAccepter = AnyAccepter()
    val stringAccepter: Accepter<String> = anyAccepter

    // here we're passing a StringConsumer, but the implementation expects Consumer<Any>
    stringAccepter.acceptWith("x", StringConsumer())
}

使用这些实现,您将获得一个声音程序,该程序在运行时将导致ClassCastException:

Exception in thread "main" java.lang.ClassCastException: class java.lang.Object cannot be cast to class java.lang.String 
    at contravariance.StringConsumer.consume(consumers.kt:27)
    at contravariance.AnyAccepter.acceptWith(consumers.kt:23)
    at contravariance.ConsumersKt.main(consumers.kt:36)

答案 1 :(得分:1)

本身允许输入的函数参数在逻辑上等同于函数的返回值,这些返回值显然处于“出”位置。

考虑以下简单示例:

interface Worker<in T> {
    fun work(output: Consumer<T>)
}

从逻辑上讲,它等同于

interface Worker<in T> {
    fun work(): T
}
在任何情况下,

work()都可以输出一个值。

此失败的示例:

fun bad(anyWorker: Worker<Any>) {
    val stringWorker: Worker<String> = anyWorker
    stringWorker.work(Consumer { value: String -> /* value could be Any since it came from anyWorker! */ })
}

但是,我们可以通过为函数引入新的类型参数来解决此问题:

interface Worker<in T> {
    fun <U : T> work(output: Consumer<U>)
}

现在,work()仅被允许使用消费者必须能够消费的Consumer某些特定子类型来调用T。例如,让我们想象一下工作像原始问题一样接受了另一个论证,并且实际上做了一些事情:

class Worker<in T> {
    private val inputs = mutableListOf<T>()

    fun <U : T> work(input: U, output: Consumer<U>) {
        inputs += input
        output.accept(input)
    }
}

通过引入类型参数U,我们可以确保inputoutput彼此一致,但仍允许Worker<Any>扩展{{1 }}。