问题
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
, asOption
is 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
T
is notB
, it isA
. The compiler is allowed to do that because of covariance of the type parameterT
.opt1
isOption1<B>
Option1<out T>
is covariant onT
, which allows substitutingT
with any supertype ofB
,This is allowed because for any
Z
such thatB : Z
,opt1
can also be treated asOption1<out Z>
thanks to theout
modifier, and the compiler can then type-check the call against a receiver typeOption1<Z>
.the substition for
T
would be the least common supertype ofB
and whateverX
such that the lambda returnsOption1<X>
,- the lambda returns
Option1<A>
, - find the least common supertype of
B
andA
, - 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 ofB
andInt
isAny
.
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