I’m trying to use a reified type parameter to check if the type argument is nullable, returning a different class implementation based on the nullability of the type argument. This works well, except for the non-null subclass requiring its generic type to have a non-null Any upper bound, in order to have a KClass<T> constructor argument.
This code works as expected:
interface Test
class NullableT<T> : Test
class NonNullT<T> : Test
inline fun <reified T> test(): Test {
return if (null is T) {
NullableT<T>()
} else {
NonNullT<T>()
}
}
test<String?>()::class.simpleName // NullableT
test<String>()::class.simpleName // NonNullT
However, this code has a compiler error:
interface Test
class NullableT<T> : Test
class NonNullT<T : Any>(tClass: KClass<T>) : Test
inline fun <reified T> test(): Test {
return if (null is T) {
NullableT<T>()
} else {
NonNullT<T>(T::class) // <-- error with <T>
// Type argument is not within its bounds. Expected: Any Found: T
}
}
Following the check for !(null is T), there needs to be some way to cast T as having a non-null Any upper bound.
It’s possible to make a non-null T optional. This works:
interface Test
class NullableT<T> : Test
class NonNullT<T : Any> : Test
inline fun <reified T : Any> test(nullable: Boolean): Test {
return if (nullable) {
NullableT<T?>()
} else {
NonNullT<T>()
}
}
test<String>(true)::class.simpleName // NullableT
test<String>(false)::class.simpleName // NonNullT
But I need a way to make a nullable T non-null. This isn’t valid:
interface Test
class NullableT<T> : Test
class NonNullT<T : Any> : Test
inline fun <reified T> test(nullable: Boolean): Test {
return if (nullable) {
NullableT<T>()
} else {
NonNullT<T!!>() // Type parameter 'T' is not an expression
}
}
import kotlin.reflect.*
interface Test
class NullableT<T> : Test
class NonNullT<T : Any>(tClass: KClass<T>) : Test
inline fun <reified T> test(dummy: Nothing? = null): Test {
return NullableT<T>()
}
inline fun <reified T: Any> test(): Test {
return NonNullT<T>(T::class)
}
fun main(){
println(test<Any>().toString())
println(test<Any?>().toString())
println(test<String>().toString())
println(test<String?>().toString())
}
“But why does this work?” I hear you asking. Well simply, what’s happening here is that the Kotlin compiler prefers functions that match the number of passed in parameters (in this case 0) over functions that have default parameters that would result in allowing the number of passed in parameters (in other words, if you call a function with 1 argument, the compiler will prefer a function with 1 parameter over a function with 2 parameters where the 2nd parameter has a default value). However, because T has a different upper bound in the 2 functions, if the type that you’re trying to invoke test with doesn’t follow the upper bound of the preferred function (which in this case is the one that returns NonNullT), the compiler will fall back to calling the more broad test function (i.e. the one that returns NullableT).
Thanks @kyay10. As I noted on Stack Overflow: True, this is hacky, but could still work, except it doesn’t in Kotlin 1.3.72 unfortunately. It fails to compile with Type argument is not within its bounds: should be subtype of 'Any' . It still tries to use the no-argument function, even though the type isn’t within the bounds. Changing the call to test<Any?>(null) forces it to call the working function. It works in 1.4-M2 and 1.4-M3, which would be promising for possibly eventually working when 1.4 is released, except it also doesn’t compile in 1.4.0-RC.
This is essentially what I’ve had to resort to. My use case is a bit more complex than my example code, where the returned Test class actually carries the T type as it’s generic type. So it requires further casting NonNullT<Any> to Test<T> essentially. But at least it compiles.
Seems there should be some mechanism in the language to allow for something like this with reified types to avoid the unchecked casting.
If you are willing to use “experimental features”, you can make @kyay10’s code compile under Kotlin 1.3.72 by enabling new inference algorithm with -XXLanguage:+NewInference compiler argument. The new algorithm is enabled by default starting from Kotlin 1.4-M1.
I’ve been trying to find a solution to solve the problem (and to even solve the more generic case of imposing an arbritary upper bound on a reified type), but sadly I haven’t reached anything. The closest I got is to literally just cast T::class as a KClass<*> (in your actual situation, however, you’d then need to cast the NonNullT result to a Test<T>).
Howeve, the good news is that it seems like the RC version on the Kotlin Playground was a bit broken because now if you run it again using the RC version (which they have, for some reason, renamed to rc instead lol), it complies and works perfectly. My guess is that they had the new type inference algorithm turned off by mistake, but it was actually supposed to be turned on for all 1.4 versions. So yeah, now my Jacky approach actually works and can even work for any arbritary upper bound.
TL;DR: RC was broken; they fixed it; my code now works and will work for the foreseeable future.
Thanks for looking into this more. Both of these solutions, while not ideal or intuitive, do allow for the code to work at least. I’ll probably stick with the casting for now, since I’m using Kotlin 1.3 still, and it keeps the function signature simpler without the extra optional parameter being seen. While the multiple unchecked casting isn’t ideal, I can at least encapsulate it all within the inline reified function and hide the lint warnings with @Suppress("UNCHECKED_CAST").
I do think there could be better language support within reified functions to allow for this type of type inference.
Yeah, there should definitely be support for a T is Any check or smthn similar that gets resolved at compile time (because it’s all inlined). The unchecked casts are slightly more elegant than needing to repeat the method with a dummy parameter, but if you need to call any other reified function, you’ll sadly have to use the dummy parameter method if you need to keep the type info.