Author profile picture

Out-Projection

An out-projection is a kind of type projection that makes a generic covariant, but also causes any functions that accept the type parameter to be unusable in that context.

You can think of it this way - In regular life, when some three-dimensional thing casts a shadow, that shadow is a two-dimensional projection of the original object - it’s an incomplete representation of it. In the same way, an out-projection is a limited view of the original class. The benefit is that it buys us the ability to accept subtypes. Let’s take a look at an example.

Example

class VetClinic {
    fun examine(patients: Array<out Animal>) {
        println("The first patient is ${patients[0].name}")
    }
}

By adding the out variance annotation to the type argument Animal, our VetClinic can now accept an Array<Dog> or Array<Cat> in addition to Array<Animal>:

val dogs: Array<Dog> = arrayOf(Dog("Bruno"), Dog("Bandit"))
VetClinic().examine(dogs)

Even though an Array<Dog> is not usually a subtype of Array<Animal>, at this site in our code, it will be. We call this use-site variance - because we’re adding subtyping at the point where we’re using it.

The Limitation: out means out-only

When you’re working with an out-projection, you’re not allowed to use any functions or setters that would accept that type parameter. So in other words, our examine() function is not allowed to put an Animal into the array. For example, this does not compile:

class VetClinic {
	fun examine(patients: Array<out Animal>) {
	    patients[0] = Cat("Tom") // Compiler error here
	}
}

Why can’t we do this?

There are multiple types that we have to keep in mind for our patients parameter:

  1. The declared type - in the code above, it’s Array<out Animal> - an out-projected array of Animal objects.
  2. The actual type - what we actually passed to the examine() method was Array<Dog>.

If this last version of our VetClinic were to compile, we’d be putting a Cat into an Array<Dog>! Imagine the surprise of some other code that’s using that Array<Dog> when it comes across a Cat!

Since this isn’t type-safe, the Kotlin compiler won’t allow us to do this.

When to use out-projections

An out-projection can be helpful when:

  1. You want covariance on a generic. For example, you want to accept Array<Dog> in addition to Array<Animal>.
  2. You don’t control that generic. Otherwise, you should consider using Declaration-Site Variance first.
  3. You aren’t going to need to use a setter or a function with the type parameter in the “in” position. Out-projections don’t allow these operations.

Share this article: