Today I struggled to understand Kotlin's in
and out
modifiers while learning about generics. Fortunately, I think I've finally developed an intuition around it.
Suppose we have an Animal supertype, a Cat and Dog subtype, and a Box type:
open class Animal
class Cat : Animal()
class Dog : Animal()
class Box<TAnimal : Animal>(
private var animal: TAnimal
) {
fun getAnimal(): TAnimal = animal
fun setAnimal(animal: TAnimal) {
this.animal = animal
}
}
And suppose we have a function that accepts a Box<Animal>
as an argument:
fun setAnimalFree(box: Box<Animal>) {
// do something
}
Would it be possible to call setAnimalFree
with a Box<Cat>
object?
fun main() {
val catInABox = Box(Cat())
setAnimalFree(catInABox) // ERROR: Type mismatch. Required: Box<Animal>. Found: Box<Cat>
}
Unfortunately, the above code results in an type mismatch error. Intuitively, we might think that Box<Cat>
satisfies Box<Animal>
just because Cat
satisfies Animal
. That is, we expect Box<Cat>
to be a subtype of Box<Animal>
. However, this thinking is incorrect. Let's see why.
Imagine that we have a client who works with Box<Animal>
. The client has the following expectations:
setAnimal(animal)
with any Animal
. In the following example, the client expects to set a Dog
.getAnimal()
to receive any Animal
.fun doClientStuff(box: Box<Animal>) {
box.setAnimal(Dog())
val animal: Animal = box.getAnimal()
// ...
}
fun main() {
val catInABox = Box(Cat())
doClientStuff(catInABox) // ERROR: Type mismatch. Required: Box<Animal>. Found: Box<Cat>
}
Same situation as before. We might postulate that Box<Cat>
cannot satisfy Box<Animal>
because we cannot call .setAnimal(Dog())
on it. We cannot set a Dog
on a box for cats, but we can do so for a box for any animals!
On the other hand, the client expects .getAnimal()
to return any animal that satisfies the Animal
type. Box<Cat>
meets this expectation because it returns a Cat
, which satisfies Animal
.
Based on this example, we can infer the following:
Box
that consume an Animal
type must receive a type that minimally satisfies every possible Animal
type because the client may want to set every possible animal.Box
that produce an Animal
type may return a subset of the Animal
type because the client just expects any animal.The Ins and Outs of Generic Variance in Kotlin explains this logic quite well.
The only way for Box<Cat>
to satisfy Box<Animal>
is if we constrain the client to use only the methods on Box<Animal>
that produce an Animal
type, while banning methods that consume Animal
types. We can achieve this specific constraint using the out
modifier!
fun doClientStuff(box: Box<out Animal>) {
val animal: Animal = box.getAnimal()
// ...
}
fun main() {
val catInABox = Box(Cat())
doClientStuff(catInABox) // No more error! Box<Cat> satisfies Box<out Animal>!
}
Another alternative to using Box<out Animal>
would be to define our function generically:
fun <TAnimal : Animal> doClientStuff(box: Box<TAnimal>) {
val animal: TAnimal = box.getAnimal()
// ...
}
fun main() {
val catInABox = Box(Cat())
doClientStuff(catInABox)
}
If the client takes a Box<Cat>
instead and we want to pass a Box<Animal>
, what constraint can we apply to allow this? We can use the in
modifier, which constrains Box<Cat>
to consumer methods only! This works because Box<Animal>
will work when called with the consumer method .setAnimal(Cat())
. But not with the producer method .getAnimal()
, which expects only a Cat
to be returned.
fun doClientStuffV2(box: Box<in Cat>) {
box.setAnimal(Cat())
// ...
}
fun main() {
val animalInABox = Box(Animal())
doClientStuffV2(animalInABox)
}