learning kotlin & have been reading about subtypes/supertypes and variance from https://typealias.com/guides/star-projections-and-how-they-work/ & generally, this website. I have a question that I don't think is covered (or maybe I'm just confused). Suppose you have a
open class A
open class B : A()
Pretty clearly A is a supertype of B. But what about the following?
open class Foo<T : A> {
fun doSomething(temp: T)
}
open class SubFoo : Foo<B>() {
}
Is SubFoo a subtype of Foo?
fun input(input: Foo<A>)
fun output(): SubFoo<B>
val inputParam = SubFoo()
input(inputParam) // works?
val ret: Foo<A> = output() // also works??
Intuitively I think the above works as desired, and the answer to the above question is yes. But I'm not completely sure, nor do I have a concrete explanation other than it resolves in my head. Honestly there's like 3 things going on here, the typing of A/B, the typing of Foo vs SubFoo, and upper bounding, and I think I'm getting lost in it all. Thanks in advance!!
CodePudding user response:
Is SubFoo a subtype of Foo?
No because Foo
is not a type. Foo<A>
and Foo<B>
are types. Syntactically, Foo
on its own is malformed unless the type parameter (the thing that goes in the <>
) can be inferred.
In this case, SubFoo
is a subtype of Foo<B>
because it inherits from Foo<B>
. SubFoo
does not become a subtype of Foo<A>
as a result of this though, so these do not work:
open class SubFoo : Foo<B>() {
override doSomething(temp: B) {
// do something that is specific to B
}
}
fun input(input: Foo<A>) { }
fun output(): SubFoo = SubFoo()
val inputParam = SubFoo()
input(inputParam) // compiler error
val ret: Foo<A> = output() // compiler error
The idea that you also become the subtype of SomeGenericType<SuperType>
by inheriting SomeGenericType<Subtype>
is called covariance. You can make a type parameter of covariant by adding out
to it. For example, List<T>
is declared like this:
public interface List<out E>
So List<String>
is a subtype of List<Any>
.
However, this only works if it is safe to do so. In the case of Foo
, it is not safe at all for its type parameter to be covariant. Consider what would happen if val ret: Foo<A> = output()
were allowed. I could do:
open class C : A()
val ret: Foo<A> = output() // suppose this worked
ret.doSomething(C())
From the type checker's perspective, this looks all fine. ret
is a Foo<A>
, so its doSomething
takes an A
. C
inherits from A
, so it can be passed to an A
parameter.
But what actually happens when this is run? A SubFoo
is returned by output()
, and SubFoo
only accepts B
s in its doSomething
method. Oopsies!