问题
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