Search code examples
kotlingenericscovariancecontravariance

Why it is forbidden to use 'out' keyword in generics if a method excepts the type parameter as a parameter?


I'm looking for an example that can cause a problem when using out in class declaration and the class has a method that get the parameter type as argument.

Also I'm looking for an example that can cause a problem when using in in class declaration and the parameter type is a var member of the class? I think that i will be able to understand the rules only by examples


Solution

  • Suppose these are the classes we are working with:

    open class Animal
    
    class Cat: Animal() {
        fun meow() = println("meow")
    }
    

    If we create a class like this with covariant out type and the compiler allowed us to use the type as a function parameter:

    class Foo<out T: Animal> {
        private var animal: T? = null
    
        fun consumeValue(x: T) { // NOT ALLOWED
            animal = x
        }
    
        fun produceValue(): T? {
            return animal
        }
    }
    

    Then if you do this, it will be lead to an impossible situation where we are trying to call meow on an Animal that doesn't have a meow function:

    val catConsumer = Foo<Cat>()
    val animalConsumer: Foo<Animal> = catConsumer // upcasting is valid for covariant type
    animalConsumer.consumeValue(Animal())
    catConsumer.produceValue()?.meow() // can't call `meow` on plain Animal
    

    And if we create a class like this with contravariant in type and the compiler allowed us to use the type as a return value:

    class Bar<in T: Animal>(private val library: List<T>) {
        fun produceValue(): T  { // NOT ALLOWED
            return library.random()
        }
    }
    

    Then if you do this, it will lead to the compiler impossibly casting a return type to a subtype.

    val animalProducer: Bar<Animal> = Bar(List(5) { Animal() })
    val catProducer: Bar<Cat> = animalProducer // downcasting is valid for contravariant type
    catProducer.produceValue().meow() // can't call `meow` on plain Animal
    

    A property has a getter which is just like a function that returns a value, and a var property additionally has a setter, which is just like a function that takes a parameter. So val properties are not compatible with contravariance (in) and var properties are not compatible with contravariance or covariance (out). Private properties aren't encumbered by these restrictions because within the class's inner workings, the type is invariant. All the class can know about its own type is its bounds. Variance just affects how the class can be cast (viewed) by the outside world.

    So an example with val is enough to show why any property is incompatible with contravariance. You could replace val with var below and it would be no different.

    class Bar<in T: Animal>(
        val animal: T // NOT ALLOWED
    )
    
    val animalProducer: Bar<Animal> = Bar(Animal())
    val catProducer: Bar<Cat> = animalProducer // downcasting is valid for contravariant type
    catProducer.animal.meow() // can't call `meow` on plain Animal