KClass
is defined as public interface KClass<T : Any> : KDeclarationContainer, KAnnotatedElement, KClassifier
This is tricky, because the class of a String?
should be KClass<String>
, but is impossible to obtain.
Given the following 3 examples below (that should all do essentially the same work), 1 of them doesn't compile, and the others return the same runtime type.
inline fun <reified T> test1(): Any = T::class
inline fun <reified T: Any> test2(): KClass<T> = T::class
inline fun <reified T> test3(): KClass<T> = T::class // does not compile
test1<String?>() // class kotlin.String
test1<String>() // class kotlin.String
test2<String?>() // does not compile
test2<String>() // class kotlin.String
The point of the question is to ask: how can I get the runtime behavior of test1
with the compile time behavior (and safety) of test2
?
EDIT: one final addendum to the question is another example, which demonstrates the problem with getting the class of a nullable type.
inline fun <reified T> test4() {
val x = T::class // compiles, implied type of x is KClass<T>
val y: KClass<T> = T::class // does not compile with explicit type of KClass<T>
}
The call site that I am specifically having problems with is this:
class OutputContract<T>(
private val output: () -> T,
val outputType: KClass<T> // ERROR!
) {
fun invoke(): T {
return output()
}
}
inline fun <reified T> output(noinline output: () -> T): OutputContract<T> {
return OutputContract(output, T::class)
}
The only error here is with KClass<T>
, not with T::class
, which hums along just fine. I want to allow the consumer to specify nullability as part of the contract, so adding an Any
constraint will not work. If I just turn KClass<T>
into KClass<Any>
, this all works (which proves there is no runtime issue, only compile time). This is ultimately the workaround I have chosen, but it would be nice if I could actually maintain the proper type.
Your question is lacking the most important information. You show a contrived case of calling your function as myFun<String?>()
but then if that was the case you could obviously change it to not use a nullable type. So this is likely not the real use case. You oversimplified your explanation and removed the most relevant information that we need to answer your question: "What is the full method signature and what does the call site look like?"
What is missing is HOW DO YOU INFER YOUR TYPE T
? You either get it from the return value, from a method parameter, or by explicitly stating it in each call site. Therefore you have these options of getting a T: Any
to use within your function, and deciding on which is best depends on the information you did NOT show in your question.
So here are your options:
If you are inferring the type based on the return parameter, then allow the return to be nullable but do not make the reified type nullable:
// call site, any return type nullable or not
val something: String? = doSomething()
// function
inline fun <reified T: Any> doSomething(): T? {
val x: KClass<T> = T::class
// ...
}
Or if you are inferring it from an incoming parameter, do the same trick there:
// call site, any parameter type nullable or not
val param: String? = "howdy"
doSomethingElse(param)
// function
inline fun <reified T: Any> doSomethingElse(parm: T?) {
val x: KClass<T> = T::class
// ...
}
Or you are literally specifying the generic parameter (simply don't make it nullable when you type the parameter name):
// call site, any non-nullable generic parameter
doSomething<String>()
// function
inline fun <reified T: Any> doSomethingElse() {
val x: KClass<T> = T::class
// ...
}
Or use the star projection if you cannot change the generic parameter (but why couldn't you?!?):
// call site: whatever you want it to be
// function:
inline fun <reified T> test4() {
val x = T::class // compiles, implied type of x is KClass<T>
val y: KClass<*> = T::class KClass<T>
}
Both x
and y
will act the same by the way, and some methods/properties will be missing on the KClass
reference.
Three out of four of those examples give you want you desire, and I can't imagine a case where one of those won't work. Otherwise, how are you inferring the type T
? What is the pattern that does not work with the above?
Pay attention to the trick with using <T: Any>
combined with T?
in the same method signature.
Based on your last update to the question, this keeps nullability as you expected for the output
function reference but allows it to work for the KClass
:
class OutputContract<T: Any>(private val output: () -> T?, val outputType: KClass<T>) {
fun invoke(): T? {
return output()
}
}
inline fun <reified T: Any> output(noinline output: () -> T?): OutputContract<T> {
return OutputContract(output, T::class)
}
The user still has control over nullability by passing in their implementation of output that does not return nulls, Kotlin will still type check for them and behave as normal. But the call to invoke has to be checked because it is always assumed to be nullable. You can't really have it both ways, wanting nullability control on T
but using it internally as a typed KClass
but you could use it as a KClass<*>
depending on what functionality you use from the KClass
. You might not be missing anything important. You don't show what you intend to do with the KClass
so it is hard to say more on the topic. KClass
is usually not a good type to use if you think they might pass you a generic class, you should use a KType
instead.