Can i get KClass<T> for instance::class?


#1

Hi,

I want to get the Kotlin Class (KClass for an object without bounds (in/out).

The code below illustrates what I’m trying to achieve:


interface IMessageService {

    fun <T : CommandBase, R : Any> send(cmd: T, requestClass : KClass<T>, receiverClass : KClass<R>): Deferred<R>
}

// cmd::class is not working because it returns KClass<out T>, so that code does not compile
inline fun <reified R: Any> IMessageService.send(cmd: CommandBase): Deferred<R> = send(cmd, cmd::class, R::class)

I want to avoid that client code needs to call

val result = messageService.send<SwitchPluginCommand, String>(SwitchPluginCommand(id, true))

instead it could be as simple as

messageService.send<String>(SwitchPluginCommand(id, true))

Can I somehow achieve that? The implementation of the MessageService class uses a serializer/deserializer which requires KClass, so I need to provide it


#2

The problem here is that if for only one type, the type interference can not work I have to provide both types on the caller site which makes it quite verbose.

An alternative would be to provide an extension method like the following

inline fun <reified T : CommandBase, R: Any> IMessageService.send(cmd: T, receiverClass : KClass<R>): Deferred<R> = send(cmd, T::class, receiverClass)

And then call it like that

val result = messageService.send(SwitchPluginCommand(id, true), String::class)

I actually wanted to get rid of String:class, but this one here comes close


#3

I think you can solve both problems by making both type parameters reified:

inline fun <reified T : CommandBase, reified R : Any> IMessageService.send(cmd: T): Deferred<R> =
    send(cmd, T::class, R::class)

And then use type inference to avoid specifying R at the call site (and thus avoid specifying either of the two type parameters):

fun test(service: IMessageService) {
    // OK
    val string: Deferred<String> = service.send(SwitchPluginCommand(...))

    // OK
    val int: Deferred<Int> = service.send(SwitchPluginCommand(...))

    // Error! Not enough information to infer R
    val unknown = service.send(SwitchPluginCommand(...))
}

However, looking at your original code, I don’t understand why do you need the T in IMessageService.send at all and why not just use cmd::class in the implementation to get the required class instance? E.g.

interface IMessageService {
    fun <R : Any> send(cmd: CommandBase, receiverClass: KClass<R>): Deferred<R>
}

In the implementation:

class MessageServiceImpl : IMessageService {
    override fun <R : Any> send(cmd: CommandBase, receiverClass: KClass<R>): Deferred<R> {
        val requestClass: KClass<out CommandBase> = cmd::class
        ...
        ...
    }
}

This way looks more foolproof to me, especially because (recalling the original issue about the type of ::class being KClass<out T>), there’s no ambiguity in what this code should do if the actual runtime type of the command happens to differ from the static type known at compile-time, e.g. if someone would pass a command via a variable of type CommandBase to the service.


#4

Hey @udalov,

thanks for the reply. Really I didn’t thought about specifying the type together with its declaration, this of course solves it. It’s nice that this works.


Actually the signature I tried was a little bit different then what i wrote in my original post:

inline fun <reified T: CommandBase, reified R: Any> IMessageService.send(cmd: T): Deferred<R> = send(cmd, cmd::class, R::class)

But of course you are right, I could also call cmd::class at the implementation of the interface.

The problem with KClass<out T> is, that I’m using an API which requires me to provide KClass<T> instead of KClass<out T>. And if I recall correctly the compiler (or intellij) would not allow me to pass it to KClass<T>

Here is how I use KClass<T>

    private suspend fun <T : CommandBase> sendCommand(cmd: T, paramRequestId : String, klass : KClass<T>)  {
        val serializedCommand = JSON.stringify(klass.serializer(), cmd)

       / * ... */
    }

And that is the prototype of the method I’m calling

    companion object : StringFormat {
 / * ... */
        override fun <T> stringify(serializer: SerializationStrategy<T>, obj: T): String 
 / * ... */
   }

#5

Remember that the type KClass<out T> means that its value represents either class T or any subclass of T, unlike the type KClass<T> which always means the class T. When dealing with a value obj: T, you don’t know at compile time whether that value will have the class T or a subclass of T at runtime, therefore you can only assume that obj::class's type is KClass<out T>.

E.g. if you have a val command: CommandBase = SwitchPluginCommand(...), its class command::class is actually KClass<out CommandBase>. Imagining it to be KClass<CommandBase> would be incorrect because CommandBase is an abstract class, and a value cannot have an abstract class at runtime.

In cases when you’re absolutely sure that the required code never does something with the KClass<X> instance that actually depends on the value of X (for example, it can just convert the class’ qualified name to a String, which doesn’t depend on KClass’ type parameter), you can get away with an unchecked cast from KClass<out T> to KClass<T>. But that’s undesirable because it frequently signals that the API was not thought out well for values of subtypes of the required type, and a cleaner solution would be to fix the API itself.

In your last code sample, what’s the signature of the KClass.serializer extension? Also, what does the SerializationStrategy look like? I suppose we could make something covariant to make this code behave correctly even in the case when the static and the dynamic type of serialized commands do not match.