5

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.

Jayson Minard
  • 84,842
  • 38
  • 184
  • 227
Burg
  • 1,988
  • 1
  • 18
  • 22
  • 1
    Adding to the mystery, IntelliJ's add type intent suggests adding a type for x, which makes the line equal to the y one, causing an error – jrtapsell Apr 03 '18 at 00:17
  • You can use `inline fun test5(): KClass<*> = T::class` to obtain `KClass` with some `X`, although I haven't thought whether this is a type-safe way. – Naetmul Apr 03 '18 at 01:58
  • May be this is a bug or limitation? Because you does not actually have a nullable class in Java. – Joshua Apr 03 '18 at 03:02
  • Think about it for a second, Kotlin has a concept of nullable types and and non-null types, but it doesn't have nullable classes. What is it you're trying to accomplish? – Strelok Apr 03 '18 at 12:48
  • I don't care about getting the nullability. All I care to do is create a class from type `T?`. For instance, I would like to do `String?::class` and have it be identical to `String::class`. – Burg Apr 03 '18 at 15:46

2 Answers2

3

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:

  1. 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
        // ...
    }
    
  2. 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
        // ...
    }
    
  3. 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
        // ...
    }
    
  4. 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.

Image from Understanding Generics and Variance in Kotlin Image from Understanding Generics and Variance in Kotlin

Jayson Minard
  • 84,842
  • 38
  • 184
  • 227
  • Actually this use case is pretty close to what I am trying to do. I'll modify the question to show EXACTLY the code I am trying to write. – Burg Apr 04 '18 at 01:16
  • @Burg So the strategies above apply perfectly to your case. I added to the end of my answer your code refactored slightly. – Jayson Minard Apr 04 '18 at 04:43
  • @JaysonMinard "HOW DO YOU INFER YOUR TYPE T" - brilliant question, helped me with another problem, thanks! – ulmaxy Mar 04 '19 at 08:38
2

NOTE: this was an answer when the question was not stated with enough information to know what the real goal was. Left here for now in case other people come here looking for the "other" thing.

Nullability is part of KType not of KClass and therefore you want to create a KType from reified parameters. Doing so is not so easy, and this concept has been discussed in KT-15992. There is some prototype code noted in that issue written by Alexander Udalov, but it might be out of date and definitely does not include nullability.

If you convert the reified parameter to a KClass you will always lose the nullability information, so no solution around KClass on its own will get you what you want.

There appears to be a workaround to get the nullability information separately, and you could rebuild your own KType using this using T::class.createType(...) but that is a bit complicated and not yet proven in all cases. The workaround to find nullability from a reified T as presented by Ruslan Ibragimov is as follows:

inline fun <reified T : Any?> sample() {
    println("Is nullable: ${isNullable<T>()}")
}

inline fun <reified T : Any?> isNullable(): Boolean {
    return null is T
}

fun main(args: Array<String>) {
    sample<String?>()
    sample<String>()
}

// Is nullable: true
// Is nullable: false

Regardless of the approach, you still would need to end up with KType.


Experimental full solution:

I created an experimental implementation of inferring the full KType with nullability based on the sample code from both Alexander Udalov and Ruslan Ibragimov. You can see this code in Klutter library source code and it is released in Klutter version 2.5.3 where it is marked with an experimental warning.

You can view the test code to see how it is working, but basically is simple as:

val nullableString = reifiedKType<String?>()
println(nullableString.isMarkedNullable) // true

val nonNullableString = reifiedKType<String>()
println(nonNullableString.isMarkedNullable) // false
Jayson Minard
  • 84,842
  • 38
  • 184
  • 227
  • I don't care about getting the nullability. All I care to do is create a class from type `T?`. For instance, I would like to do `String?::class` and have it be identical to `String::class`. The result in `test2` shows this is possible and works as expected at runtime, but is something the compiler is rather unhappy with. – Burg Apr 03 '18 at 15:46
  • @burg The only way, given your function, that you could get a type into the function is either by inferring from the left hand side, or by providing type parameters to the function call. The left side could not be nullable because it is `KClass` and the right side is something you literally have to type as the type parameter, so why would you put a nullable there to begin with? Just omit the question mark. So your first example doesn't make enough sense, but I see you added another example, I'll look at it. – Jayson Minard Apr 03 '18 at 18:38