I am not sure if 'hard-failing' is the right word, but here is the problem I am facing. And it's taken me quite some time to reproduce this to the smallest possible example, so here it goes:
class BaseParameterizedType<T>
fun <U: BaseParameterizedType<*>> getSpecific(clazz: KClass<in U>) : U {
TODO()
}
fun example(arg: KClass<out BaseParameterizedType<*>>)) {
getSpecific(arg.innerType)
}
Ok, so the code above fails at the 'TODO', but if it wasn't there and if the function returned normally, then it definitely fails with a null pointer exception. I tried hard to figure out what was going wrong, so I turned to the decompiled Java code (from the kotlin bytecode):
public static final void example(@NotNull KClass arg) {
Intrinsics.checkParameterIsNotNull(arg, "arg");
getSpecific(arg.getInnerType());
throw null; // <-- The problem
}
If I change the function signature of getSpecific(clz: KClass<in U>) : U
to any of these forms:
getSpecific(clz: KClass<out U>) : U
getSpecific(clz: KClass<U>) : U
getSpecific(clz: KClass<in U>) : BaseParameterizedType<*>
or even the function to example(arg: KClass<out BaseParameterizedType<*>)
or example(arg: KClass<BaseParameterizedType<*>>)
, then the generated code is:
public static final void example(@NotNull KClass arg) {
Intrinsics.checkParameterIsNotNull(arg, "arg");
getSpecific(arg.getInnerType());
}
Now, let's say at the call-site, I change it to:
getSpecific(BaseParameterizedType::class)
then this also DOES NOT generate the throw null
clause. So, I'm guessing this has something to do with kotlin assuming that this cast will always fail or that there is indeterminate information available to make the inference?
So, we know that arg.innerType
is KClass<out BaseParameterizedType<*>>
and we use it at a site accepting KClass<in BaseParameterizedType<*>>
, so why isn't U
inferred to BaseParamterizedType<*>>
. That is literally the only type that will ever match.
At the same time, I think just generating a throw null
statement is unbelievably difficult to debug. The stacktrace would just point to the line where there is getSpecific
and good luck figuring out where the null pointer exception came from.
Just as @hotkey mentioned, out
means in Nothing
and Nothing
will throw null.So I do some tests like this:
fun main(args: Array<String>) {
tryToReturnNothing()
}
fun tryToReturnNothing(): Nothing{
TODO()
}
Generate ->
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
tryToReturnNothing();
throw null; // here
}
@NotNull
public static final Void tryToReturnNothing() {
throw (Throwable)(new NotImplementedError((String)null, 1, (DefaultConstructorMarker)null));
}
Considering the type of null
is Nothing?
, we can return Nothing?
instead of Nothing
. So I change U
into U?
, and then the throw null
clause disappear:
fun <U: BaseParameterizedType<*>> getSpecific(clazz: KClass<in U>) : U? { // see here: change U to U?
TODO()
}
fun example(arg: KClass<out BaseParameterizedType<*>>) {
getSpecific(arg)
}
Generate ->
@Nullable
public static final BaseParameterizedType getSpecific(@NotNull KClass clazz) {
Intrinsics.checkParameterIsNotNull(clazz, "clazz");
throw (Throwable)(new NotImplementedError((String)null, 1, (DefaultConstructorMarker)null));
}
public static final void example(@NotNull KClass arg) {
Intrinsics.checkParameterIsNotNull(arg, "arg");
getSpecific(arg);
}
This is a known issue regarding the the type inference corner case handling when the inferred type is Nothing
(and it is in your case):
The inference behaves in this way because of a coercion attempt for the projections KClass<in U>
and KClass<out BaseParameterizedType<*>>
.
Basically, an out
-projected type at the same time means in Nothing
(because the actual type argument can be any of the subtypes, and nothing can be safely passed in). So, to match KClass<out BaseParameterizedType<*>>
with KClass<in U>
the compiler chooses U := Nothing
, implying that the function call returns Nothing
as well.
Remark: a Foo<out Any>
projection cannot match Foo<in T>
with T := Any
, because the actual type argument of the value passed for Foo<out Any>
can be, for example, Int
. Then, if Foo<T>
accepts T
in some of its functions, allowing the aforementioned match will also allow you to pass Any
instances to where Foo<Int>
does not expect them. Actually, in Nothing
becomes the only way to match them, because of the unknown nature of the out
-projected type.
After that, for a Nothing
-returning function call, the compiler inserts that throw null
bytecode to make sure the execution does not proceed (evaluating a Nothing
-typed expression is supposed to never finish correctly).
See the issues: KT-20849, KT-18789
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With