问题
I've been recently learning Kotlin, while having some questions with covariant type.
The sample code is here.
I have Option and Option2 both having a type parameter T and a run extension.
I could understand the first two run in validation(), since they are behaved as Java.
But why does the third line compile? Option<T> is invariant in T. We cannot passing Option<C> instance into where Option<B> is expected.
After I add an out keyword for T, now all of them could compile. Why?
open class A
open class B : A()
open class C : B()
class Option<T>(val item: T)
fun <T> Option<T>.run(func: (Int) -> Option<T>): Option<T> = func(1)
class Option1<out T>(val item: T) //out keyword
fun <T> Option1<T>.run(func: (Int) -> Option1<T>): Option1<T> = func(1)
fun validation() {
val opt: Option<B> = Option(B())
opt.run { Option(A()) } //won't compile as expected
opt.run { Option(B()) } //return type is Option<B>
opt.run { Option(C()) } //return type is Option<B>; why could this compile?
val opt1: Option1<B> = Option1(B())
opt1.run { Option1(A()) } //return type is Option<A>; why could this compile?
opt1.run { Option1(B()) } //return type is Option<B>
opt1.run { Option1(C()) } //return type is Option<B>
}
回答1:
opt.run { Option(C()) } //return type is Option<B>; why could this compile?Here, you can approximate the behavior as follows by decomposing the call into the two lines that are type-checked separately:
val func: (Int) -> Option<B> = { Option(C()) } opt.run(func)The first line is correct because:
- the lambda is expected to return
Option<B>(with exactlyB, asOptionis invariant), - so the
Option(item: T): Option<T>constructor call needs to accept aB, - the argument that is passed is
C(), - as
C : B,C()passes the check for beingB, - and so
Option(C())can also be typed asOption<B>and passes the check, - OK, the lambda passes the check for
(Int) -> Option<B>.
Sanity check: what if you replace the first line as follows?val func: (Int) -> Option<B> = { Option(C()) as Option<C> }Then it won't get compiled, as the expression inside the lambda is now typed as
Option<C>which is not a subtype ofOption<B>.- the lambda is expected to return
opt1.run { Option1(A()) } //return type is Option<A>; why could this compile?In this sample, the type that the compiler chose for
Tis notB, it isA. The compiler is allowed to do that because of covariance of the type parameterT.opt1isOption1<B>Option1<out T>is covariant onT, which allows substitutingTwith any supertype ofB,This is allowed because for any
Zsuch thatB : Z,opt1can also be treated asOption1<out Z>thanks to theoutmodifier, and the compiler can then type-check the call against a receiver typeOption1<Z>.the substition for
Twould be the least common supertype ofBand whateverXsuch that the lambda returnsOption1<X>,- the lambda returns
Option1<A>, - find the least common supertype of
BandA, - given that
B : A, the least common supertype isA - substitute
T := A.
Sanity check: what if you change the expression as follows?
opt1.run { Option1(0) }It will still compile successfully, but the inferred return type will be
Option1<Any>. This is totally reasonable according to the above, because the least common supertype ofBandIntisAny.
Disclaimer: this is not how the compiler works internally, but using this way of reasoning you may often get the results that agree with the compiler's results.
来源:https://stackoverflow.com/questions/55765128/kotlin-generics-counterintuitive-type-inference-and-checking-with-out-keyword