There are lots of examples of Kotlin generics but this is one I’ve not come across.
I’ve simplified it down to this:
open class Fruit
class Apple : Fruit()
class Orange : Fruit()
abstract class Helper<T : Fruit> {
open fun save(something: T) {}
}
class AppleHelper : Helper<Apple>() {
override fun save(something: Apple) {}
}
class OrangeHelper : Helper<Orange>() {
override fun save(something: Orange) {}
}
fun process(theFruit: Fruit) {
val myHelper: Helper<out Fruit>
when (theFruit) {
is Apple -> {
myHelper = AppleHelper()
myHelper.save(theFruit) // this does compile (NOT expected)
}
is Orange -> {
myHelper = OrangeHelper()
myHelper.save(theFruit) // this does compile (NOT expected)
}
else -> throw RuntimeException()
}
myHelper.save(theFruit) // this doesn't compile (as expected)
}
Using the out keyword for the myHelper variable makes it a producer of Fruit, however the 2 locations marked allow it to act as a consumer of theFruit.
The third location, as expected, rejects theFruit into the save method.
So clearly Kotlin is clever and can tell within the blocks that myHelper can be a safe consumer – but where is this behaviour documented?
>Solution :
This is because of Kotlin’s smart casts (documentation and specification).
The compiler does some dataflow analysis on your code and concluded that after the line myHelper = AppleHelper(), myHelper must be of type AppleHelper, theFruit must also be of type Apple because this is in the is Apple branch of the when. Therefore, myHelper.save(theFruit) is valid.
Whether or not generics is used is not relevant. Kotlin does this for everything. e.g.
val randomNumber = Random.nextInt()
val foo: Any
when (randomNumber) {
1 -> {
foo = "String" // here foo will be String
println(foo.length) // can use properties specific to String
}
2 -> {
foo = 10 // here foo will be Int
println(foo * foo) // can multiply Ints
}
else -> {}
}