Generic variance: can't use var with <out T> and can't use val with <in T>?

This is literally it lol.

Yes, they are referencing the same value. The thing about generics is that they are kind of a “contract-y” thing, i.e. the Container class actually has no idea what T is. Even if we assume that it did know what T is (which it does in .NET languages like C#, but sadly not in Kotlin), it would only know what T was when the object was created (i.e. T would basically kinda be like a val constructor parameter). Casting basically just allows you to look at an object in a more constrained or less constrained way, and so it never, and I mean never, mutates the object or copies it or anything, it just gives you a different “perspective” of the object.

Oh, the horse moved again! Beat it some more!

Ok. I’ve been testing this stuff out in my own code. I felt that Any, representing everything, was too general for me to see how things were working. I made up this code:

open class AnyAnimal
class Dog : AnyAnimal()
class Cat : AnyAnimal()

class Container<out T>(val species: T)

fun main() {
    val kennel: Container<Dog> = Container(Dog())
    val zoo : Container<AnyAnimal> = kennel

    val animals : AnyAnimal = zoo.species

    if (animals is Dog) {
        println("It's a doggy!")
    }
}

The val animals is type AnyAnimal. Yet, this if statement here (animals is Dog) does result to true. Yet if I change it to (animals is Cat), it is false, and the println output doesn’t happen.

I found this interesting. A Dog certainly is an AnyAnimal; I didn’t think AnyAnimal would also be a Dog.

So, because kennel, of type Container<Dog> is cast to zoo, of type Container<AnyAnimal>, the animals val and therefore zoo.species has 2 types? And that is what happens with casting?

1 Like

You’re almost there. There are basically 2 different types of types (types of types? kinds of types I guess is the better way to put it). Those 2 are compile time and runtime types. During compile time, you only have the times that are specifically declared in your code. Safe casts are safe because they are guaranteed at compile time. During runtime though, you can see what every object’s real type is, but as a downside you can’t see the type parameters of an object. animals is Dog is a runtime check. It basically checks if the real type of animals is actually a Dog. This has no realtion whatsoever to the fact that zoo.species is an AnyAnimal. zoo could literally be a Container<Any> and the code would still work in the same way. The reason behind that is that zoo is referencing the Container(Dog)) that you created before, and so when you check whether zoo.species is a Dog, your program checks during runtime whether the reference that zoo.species has is of type Dog, and in this case it is.

is basically allows you to check if a cast would actually be legal or not. Because Kotlin also has smartcasting, inside of the if (animals is Dog) block, you can use animals as a Dog object, because the compiler knows that during runtime this block of code will only run if animals is indeed a Dog. You couldn’t use animals as a Dog before doing the check tho, because the compiler has no way of guaranting that an AnyAnimals would definitely be a Dog without knowing that that specific instance of AnyAnimals (which is animals) is a Dog.

Why are you ignoring the compiler error in the first line? What you need to understand is that not every class can be contravariant or covariant.

The compiler will check the structure of your class and will allow or disallow it to have “in” or “out” type parameters.

Generally, classes that have T only in “in” positions are allowed to be contravariant on T. Classes that have T only in “out” positions can be covariant on T. Classes that have T in both positions can neither be covariant or contravariant.

In your example, this class has T in an “out” positions (a property accessor). Therefore it is not allowed to declare “in T”.

And there are good reasons for this, too. You have found the reasons yourself. This very example shows why this class cannot be contravariant. If it were contravariant, there would be a failure in the last line of this example. But lucky for us, the compiler prevents this because the first line is not accepted by the compiler.

The rest of your questions in this post are invalid, because there cannot be a situation in which they could be asked (the compiler prevents such situations).

Here is an example of a class that is allowed to be contravariant:

class Consumer<in T> {
   fun consume(t: T)  // T is used in "in" position only
}

This code was given as an example of why you can’t use a ‘val’ with <in> to help me understand the concept. I’m not ignoring the error; the error was introduced intentionally to illustrate the point.
I don’t think my questions are invalid. I’m trying to understand the underlying concepts, not trying to get intentionally broken code to work. Just because you understand it well, and I don’t, does NOT make my questions invalid.

1 Like

I might have misunderstood your intentions in that post, and I am sorry for that. I do not think that your questions are invalid in general. Quite contrary.

I still think though that those questions should be discussed in the context of valid code. This is also important for preventing potential misunderstanding with future readers of these posts.

1 Like

Normally I agree with that. However to understand restrictions you have to understand what they are suppost to prevent. Therefore when looking at variance it is a necesseary thought experiment to ignore variance to see what kind of errors that would create. I guess in this case it’s possible to use the UnsafeVariance anotation :innocent:

class Container<in T>(var content: @UnsafeVariance T) 

fun main() {
/* 1 */  val cont = Container<Any>(5) // Just creating a new Any object for this example
/* 2 */  val contString: Container<String> = cont //This is totally legal because in guarantees downcasting safely
/* 3 */  val str: String = contString.content //Whoops, you are now trying to cast an Any to a String.
}

If we look at what happens in the main method it’s something like:

  1. create a container with generic type any and fill it with a value of 5
    This means that the we save a value of type Int inside a field of type Any. So far so good.
  2. Cast the container from Container<Any> to Container<String>. This means that content still contains the value 5 of type Int but our program now thinks it’s a String. This is bad, but it won’t raise any errors at this point. If we were to cast this back to a Container<Any> or Container<Int> we would never notice something is wrong.
  3. When we access contSTring.content this is basically the same as trying contString.content as String. The value of content is 5 but the program thinks it’s a String which will result in an exception.

For that reason you aren’t allowed to use in T as a return type (or the type of a property). Same goes the other way round. You can’t use out T is a parameter to a function (or a var property) since you can create a similar situation where this would result in a typecast exception.


To prevent the question about the UnsafeVariance annotation I used: There are some few exceptions to this rule, eg. equality checks. This annotation allows for functions like List<T>.contains(value: @UnsafeVariance T). T shouldn’t be allowed in an in position for Lists but in this case it’s ok.
This annotation should still only be used very cautiously and if you really understand how generics work and are implemented on each platform(jvm/js/native).

2 Likes

Ah, so you are saying that because zoo references kennel, that zoo is referencing an object of type Container<Dog>, even though zoo is defined as a Container<AnyAnimal>. zoo could be defined as a Container<Any>, but would still be referencing a Container<Dog>. So animal is therefore referencing a Dog when it access zoo.species. Therefore, of course (animal is Dog) will be true. And as a Dog is a subtype of AnyAnimal and also a subtype of Any by definition, it will always be true anyway that (animal is AnyAnimal) or (animal is Any). Like so:

open class AnyAnimal
class Dog : AnyAnimal()
class Cat : AnyAnimal()

class Container<out T>(val species: T)

fun main() {
    val kennel: Container<Dog> = Container(Dog())
    val zoo : Container<Dog> = kennel

    val animals : Dog = zoo.species

    if (animals is AnyAnimal) {
        println("A Dog is AnyAnimal!")
    }
}
1 Like

I see there are some great explanations in this topic, but I thought I would drop this picture, as it visualizes variance in quite an intuitive way:

Source: scala - How to check covariant and contravariant position of an element in the function? - Stack Overflow

I would also recommend looking into it, as it supports the image with code examples (in Scala, but they should be understandable) and comments.

2 Likes