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))
Can I somehow achieve that? The implementation of the MessageService class uses a serializer/deserializer which requires KClass, so I need to provide it
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
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.
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.
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:
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
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.