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:
- The declared type - in the code above, it’s
Array<out Animal>
- an out-projected array ofAnimal
objects. - The actual type - what we actually passed to the
examine()
method wasArray<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:
- You want covariance on a generic. For example, you want to accept
Array<Dog>
in addition toArray<Animal>
. - You don’t control that generic. Otherwise, you should consider using Declaration-Site Variance first.
- 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.