Reputation: 4879
I'm trying to build an event handler with events indexed by class. You would register to listen for events of class InterestingEvent : Event
(for example) and would be notified whenever a new InterestingEvent()
was sent to the manager.
I can't get the Kotlin generic code to work right though. This is the best I can do so far:
class EventManager{
private val listeners: MutableMap<KClass<out Event>, MutableList<EventListener<in Event>>> = HashMap()
fun <T : Event> register(event: KClass<out T>, listener: EventListener<T>) {
val eventListeners: MutableList<EventListener<T>> = listeners.getOrPut(event) { ArrayList() }
eventListeners.add(listener)
}
fun notify(event: Event) {
listeners[event::class]?.forEach { it.handle(event) }
}
}
My getOrPut
call wants a MutableList<EventListener<T>>
but found MutableList<EventListener<in Event>>
instead.
Ideally I'd get rid of the KClass parameter to register
as well but I don't think that's possible.
Is it possible to do what I'm trying here?
Upvotes: 2
Views: 6594
Reputation: 4879
Pavlov Liapota also answered my question in the official Kotlin slack. Here is his solution:
class EventManager{
private val listeners: MutableMap<KClass<out Event>, MutableList<EventListener<Event>>> = HashMap()
inline fun <reified T : Event> register(listener: EventListener<T>) {
register(T::class, listener)
}
fun <T : Event> register(eventClass: KClass<out T>, listener: EventListener<T>) {
val eventListeners = listeners.getOrPut(eventClass) { ArrayList() }
eventListeners.add(listener as EventListener<Event>)
}
fun notify(event: Event) {
listeners[event::class]?.forEach { it.handle(event) }
}
}
Upvotes: 2
Reputation: 85966
This is a bit difficult because you want to store a mix of EventListener<T: Event>
items in lists within the same map, so that T
is unknown for any given list. Then you want to use the items from the list as if T
is known so it can be passed to notify()
. So this causes trouble. A lot of libraries just notify the generic event and the receiver does the casting work. But you can do it in a few ways to take that burden into your notification library instead since it knows that logically there is a guarantee of the relationship between the key of the map and the list item types.
Here is some adjusted code with two forms of the notify.
class EventManager {
val listeners: MutableMap<KClass<*>, MutableList<EventListener<out Event>>> = mutableMapOf()
inline fun <reified T : Event> register(listener: EventListener<T>) {
val eventClass = T::class
val eventListeners: MutableList<EventListener<out Event>> = listeners.getOrPut(eventClass) { mutableListOf() }
eventListeners.add(listener)
}
inline fun <reified T: Event> notify(event: T) {
// here we have an unsafe action of going from unknown EventListener<T: Event> to EventListener<R>
// which we know it is because of our code logic, but the compiler cannot know this for sure
listeners[event::class]?.asSequence()
?.filterIsInstance<EventListener<T>>() // or cast each item in a map() call
?.forEach { it.handle(event) }
}
// or if you don't know the event type, this is also very unsafe
fun notifyUnknown(event: Event) {
listeners[event::class]?.asSequence()
?.filterIsInstance<EventListener<Event>>()
?.forEach { it.handle(event) }
}
}
Given some simple sample classes:
open class Event(val id: String)
class DogEvent : Event("dog")
class CatEvent: Event("cat")
interface EventListener<T: Event> {
fun handle(event: T): Unit
}
The following test works:
fun main() {
val mgr = EventManager()
mgr.register(object : EventListener<DogEvent> {
override fun handle(event: DogEvent) {
println("dog ${event.id}")
}
})
mgr.register(object : EventListener<CatEvent> {
override fun handle(event: CatEvent) {
println("cat ${event.id}")
}
})
mgr.notify(Event("nothing")) // prints: <nothing prints>
mgr.notify(CatEvent()) // prints: cat cat
mgr.notify(DogEvent()) // prints: dog dog
mgr.notifyUnknown(CatEvent()) // prints: cat cat
mgr.notifyUnknown(DogEvent()) // prints: dog dog
}
This works well since it is a self-contained system where the complete disregard we show for the type safety is not really exposed in dangerous ways and therefore is "safe enough." If I think of a better way, I'll update the posting. One method would be to create a new Map-of-class-to-lists that had a contract associated that tries to connect the value type with the key type and helps eliminate the casting.
Upvotes: 4