Eitanos30
Eitanos30

Reputation: 1439

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

Upvotes: 3

Views: 597

Answers (2)

Tenfour04
Tenfour04

Reputation: 93581

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

Upvotes: 5

Joffrey
Joffrey

Reputation: 37680

Small reminder about variance

When you have a generic class G<T> (parameterized type), the variance is about defining a relationship between the hierarchy of the types G<T> for different Ts, and the hierarchy of the different Ts themselves.

For instance, if child class C extends a parent P then:

  • does List<C> extend List<P>? (List<T> would be covariant in T)
  • or the reverse? (contravariant)
  • or is there no relationship between List<C> and List<P>? (invariant).

Example

Now, consider List<out T>, which means that List is covariant in T. As we've just seen, declaring list as such means that the following holds: "if C extends P, then List<C> extends List<P>".

Let's assume the following class declarations here:

open class Parent {
    fun doParentStuff()
}

class Child : Parent() {
    fun doChildStuff()
}

The covariance of List<out T> means that this is possible:

val listOfChild: List<Child> = listOf<Child>(Child(), Child())
// this is ok because List is covariant in T (out T)
// so List<Child> is a subtype of List<Parent>, and can be assigned to listOfParent
val listOfParent: List<Parent> = listOfChild 

So what would happen if we could declare a method in the List class that accepts a parameter T?

class List<out T> {
    fun add(element: T) {
        // I can guarantee here that I have an instance of T, right?
    }
}

The rules of most languages (including Kotlin) state that if a method accepts a parameter of type T, you can technically get an instance of T or any subclass of T (this is the point of subclassing), but you have at least all the API of T available to you.

But remember that we declared List<out T>, which means I can do:

val listOfChild: List<Child> = listOf<Child>(Child(), Child())
// this is ok because List is covariant in T (out T)
val listOfParent: List<Parent> = listOfChild

// listOfChild and listOfParent point to the same list instance
// so here we are effectively adding a Parent instance to the listOfChild
listOfParent.add(Parent()) 

// oops, the last one is not an instance of Child, bad things will happen here
// we could fail right here at runtime because Parent cannot be cast to Child
val child: Child = listOfChild.last

// even worse, look at what looks possible, but is not:
child.doChildThing()

Here you can see that from within the List<Child> instance, we actually could receive an instance of Parent which is not a subclass of Child in a method that had declared a parameter of type Child.

Upvotes: 3

Related Questions