Why are contravariant type parameters in function parameters considered in “out” position?

纵然是瞬间 提交于 2020-02-02 11:21:49

问题


Hard for me to describe in english, but here's the issue:

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>) {}
}

It can be fixed like this:

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

But I don't understand the issue. It doesn't seem unsafe to allow Consumer<T>. Can someone explain this?


回答1:


The argument position is called contravariant because its variance goes in contrary direction w.r.t. the class variance. It means that the supertypes of a class can take the subtypes of an argument type as a parameter and vice versa.

Let's consider some actual parameter type S. In this example a type Accepter<S>, which is a supertype of Accepter<Any>, must take a subtype of Consumer<Any> as a parameter, but with the given signature it takes Consumer<S>, that isn't a subtype of Consumer<Any>, but rather a supertype of it.

Another example why this argument type would be unsafe if allowed. Let's consider the following implementations of Accepter and Consumer:

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())
}

With these implementations you'll get an unsound program, which will result in ClassCastException at run time:

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)



回答2:


Function parameters which themselves allow input are logically equivalent to return values for a function, which are obviously in "out" position.

Consider this simple example:

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

This is logically equivalent to

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

work() can output a value in either case.

An example of this failing:

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

However, we can solve this by introducing a new type parameter for the function:

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

Now, work() will only be allowed to call the Consumer with some specific subtype of T that the consumer must be able to consume. For example, lets imagine that work takes another argument, as in the original question, and actually does something:

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

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

By introducing the type parameter U, we can ensure that input and output are consistent with respect to each other, but still allow Worker<Any> to extend Worker<String>.



来源:https://stackoverflow.com/questions/55961416/why-are-contravariant-type-parameters-in-function-parameters-considered-in-out

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!