TL;DR Should functions with reified types take account of type-parameter nullability when generating code?
Test case
Consider the following Kotlin code; the only difference between the two methods is whether the type bound is nullable (Any?
) or not (Any
).
@Test
fun testNonNullableBound() {
val x: Int = nonNullableBound()
}
@Test
fun testNullableBound() {
val x: Int = nullableBound()
}
private inline fun <reified T : Any> nonNullableBound(): T {
return unsafeMethod()
}
private inline fun <reified T : Any?> nullableBound(): T {
return unsafeMethod()
}
where unsafeMethod
subverts the type system by being defined in Java:
public static <T> T unsafeMethod() { return null; }
This is Kotlin 1.1.4.
Expected behaviour
I'd expect these to behave equivalently - the type is reified, so the actual value of T
is known to be non-nullable, so a null check ought to be applied inside the function before the return
statement.
Observed behaviour
The two cases fail in different ways:
testNonNullableBound
behaves as expected (fails due to a null check on the value returned byunsafeMethod()
).testNullableBound
doesn't behave as expected - it fails with an NPE when doing the assignment tox
.
So it appears the insertion of null checks is based on the type bound, rather than the actual type.
Analysis
For reference, the relevant bytecode is as follows. Note the null check added in testNonNullableBound
.
testNonNullableBound
public final testNonNullableBound()V
@Lorg/junit/Test;()
[...]
L1
LINENUMBER 28 L1
INVOKESTATIC JavaStuff.unsafeMethod ()Ljava/lang/Object;
DUP
LDC "unsafeMethod()"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkExpressionValueIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
[...]
testNullableBound
public final testNullableBound()V
@Lorg/junit/Test;()
[...]
L1
LINENUMBER 27 L1
INVOKESTATIC JavaStuff.unsafeMethod ()Ljava/lang/Object;
[...]
Exactly right, this is how it is supposed to work!
A JVM function cannot have different byte code depending on the actual generic type, so the same implementation has to satisfy all possible outcomes.
Inlining doesn't affect this case because it follows the same rules as normal functions. It was designed in this way so that the developer is less surprised.