Home > Enterprise >  Kotlin put children class type into parent class type
Kotlin put children class type into parent class type

Time:05-12

I'm trying to have an object that collects the listeners for the childrent class of Message class.

typealias Listener <T> = (T) -> Unit

object MessageRegistry {
    private val listeners = mutableMapOf<KClass<out Message>, MutableSet<Listener<Message>>>()

    fun <T : Message> listen(clazz: KClass<T>, listener: Listener<T>) {
        listeners.getOrPut(clazz) { mutableSetOf() }.add(listener)
    }
}

I want the clients of this object to register their listeners like the following when ConnectedMessage is a subtype of Message:

MessageRegistry.listen(ConnectedMessage::class) { connectedMessage: ConnectedMessage ->
  doSomething(connectedMessage)
}

However, there seems to be a problem in the MessageRegistry.listen() method. I can't get listener added to the MutableSet<Listener<Message>>>.

The listener has a type (T) -> Unit with a generic T being <T: Message> and
I want to added to the MutableSet with the type (Message) -> Unit.

I'm getting a type error as follows:

Type mismatch.
Required:
Listener<Message> /* = (Message) → Unit */
Found:
Listener<T> /* = (T) → Unit */

As I mentioned that the T inherits the Message class (<T : Message>), I think this is a valid conversion.


I've also tried marking out to the generic type of the Listener:

private val listeners = mutableMapOf<KClass<out Message>, MutableSet<Listener<out Message>>>()

but it gives the following error:

Conflicting projection in type alias expansion in intermediate type '(T) -> Unit'

What can I do to have the listeners in a set?

Thanks in advance.

CodePudding user response:

Your Listener is a T consumer, not producer, so it cannot be given a type of out Message and still be useful.

Since your function allows subtypes of Message for the Listener, but Listener is a consumer, your Listener cannot satisfy the requirement to be a Listener<Message>. For example, if you had a Listener<SubMessage> and tried to pass a Message to it, it would be unsafe because the Message might not be a SubMessage.

However, since you are storing these in a Map with the class, you are effectively creating runtime-safety of the types, and you can logically deduce types are safe to cast if they match up.

Therefore, the generics system cannot guarantee safety, but you can if you are careful about where you cast. You can implement it like this:

object MessageRegistry {
    private val listeners = mutableMapOf<KClass<out Message>, MutableSet<Listener<*>>>()

    fun <T : Message> listen(clazz: KClass<T>, listener: Listener<T>) {
        listeners.getOrPut(clazz) { mutableSetOf() }.add(listener)
    }

    private fun <T: Message> getListeners(clazz: KClass<T>): MutableSet<Listener<T>> {
        @Suppress("UNCHECKED_CAST")
        return listeners[clazz] as MutableSet<Listener<T>>
    }
}

I suggest making these inline/reified to make your code simpler at the call sites. In most cases, it will be able to infer the type at the call site so you won't have to explicitly pass it.

object MessageRegistry {
    private val listeners = mutableMapOf<KClass<out Message>, MutableSet<Listener<*>>>()

    @PublishedApi
    internal fun <T : Message> listen(clazz: KClass<T>, listener: Listener<T>) {
        listeners.getOrPut(clazz) { mutableSetOf() }.add(listener)
    }
    
    inline fun <reified T : Message> listen(noinline listener: Listener<T>) {
        listen(T::class, listener)
    }

    private fun <T: Message> getListeners(clazz: KClass<T>): MutableSet<Listener<T>> {
        @Suppress("UNCHECKED_CAST")
        return listeners[clazz] as MutableSet<Listener<T>>
    }
}
  • Related