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>>
}
}