So I was playing around generics in Kotlin. Let's say I have a typical Box
type:
class Box<T>
Now, I understand I can use the *
projection in a function when I do not care about any methods or properties that include the T
type.
So for example, if a have a takeBox1
that takes a box and does not use T at all, I could declare it as:
fun takeBox1(box: Box<*>) {}
And then later if I have a specific T
Box I can provide because takeBox1
does not care what T
it is:
fun <T> giveBox1(box: Box<T>) {
takeBox1(box)
}
However, it gets weird when I have a doubly-wrapped box, i.e. a takeBox2
that takes Box<Box<*>>
, it does not accept a specific Box<Box<T>>
:
fun takeBox2(box: Box<Box<*>>) {}
fun <T> giveBox2(box: Box<Box<T>>) {
takeBox2(box) // compiler error!
}
Which is quite weird, if it can accept Boxes of Boxes of anything, why can't it accept a Box of Box of something? Note that again I am not using T at all in the implementation - maybe I was just doing box.size
or something.
After more testing, it does work if I declare the outer box as out
, but only if the outer Box
is the OutBox
; so:
class OutBox<out T>
fun takeOutBox(box: OutBox<Box<*>>) {}
fun <T> giveOutBox2(box: OutBox<Box<T>>) {
takeOutBox(box) // works
}
fun takeBoxOut(box: Box<OutBox<*>>) {}
fun <T> giveBoxOut2(box: Box<OutBox<T>>) {
takeBoxOut(box) // does not compile
}
fun takeOutBoxOut(box: OutBox<OutBox<*>>) {}
fun <T> giveOutBoxOut2(box: OutBox<OutBox<T>>) {
takeOutBoxOut(box) // works
}
If I make it in
, it does not work at all in any cases.
I've read the documentation on Kotlin Generics, specifically about in
, out
, and star projection (*
), and I thought I had a good grasp on it, but I cannot seem to justify/explain this particular behaviour. I also read other answers about the behaviour of *
but I don't understand why a doubly-wrapped Box
would behave differently than a singular Box
.
Which is quite weird, if it can accept Boxes of Boxes of anything, why can't it accept a Box of Box of something? Note that again I am not using T at all in the implementation - maybe I was just doing
box.size
or something.
The compiler doesn't care that what you are doing with the box. It only looks at the types. The types describe all the possible things you can do with a value, and in this case, the types do not ensure that "I was just doing box.size
or something".
The problem with Box<Box<*>>
is that it allows you to input a Box<U>
where U
is any type, not necessarily the T
declared in giveBox2
.
Suppose Box
is
class Box<T>(var boxed: T)
If your code were allowed to compile, one could pass a Box<Box<String>>
to giveBox2
, and takeBox2
could do:
box.boxed = Box(100)
and after that you end up with a variable of type Box<Box<String>>
that actually contains a Box<Box<Int>>
.
"But I am not actually going to do things like box.boxed = Box(100)
!" you might say. Then you should tell the compiler that, by changing the parameter type of takeBox2
to something like Box<out Box<*>>
.
If you are only going to only use size
on the outer Box
, then you can go a step further and just change it to Box<*>
.