Элегантный обработчик событий с дженериками в Котлине - PullRequest
0 голосов
/ 07 декабря 2018

Я пытаюсь создать обработчик событий с событиями, проиндексированными по классу.Вы регистрируетесь для прослушивания событий класса InterestingEvent : Event (например) и будете получать уведомления всякий раз, когда администратору отправляется новый InterestingEvent().

Я не могу заставить общий код Kotlin работать правильнохоть.Это лучшее, что я могу сделать на данный момент:

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

Мой getOrPut вызов хочет MutableList<EventListener<T>>, но вместо него нашел MutableList<EventListener<in Event>>.

В идеале я бы избавился отПараметр KClass также равен register, но я не думаю, что это возможно.

Возможно ли сделать то, что я пытаюсь здесь?

Ответы [ 2 ]

0 голосов
/ 07 декабря 2018

Павлов Ляпота также ответил на мой вопрос в официальной слабине Котлина.Вот его решение:

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) }
    }
}
0 голосов
/ 07 декабря 2018

Это немного сложно, потому что вы хотите сохранить набор из EventListener<T: Event> элементов в списках на одной карте, так что T неизвестно для любого данного списка.Затем вы хотите использовать элементы из списка, как если бы T было известно, поэтому его можно передать 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) }
    }
}

Для некоторых простых примеров классов:

open class Event(val id: String)
class DogEvent : Event("dog")
class CatEvent: Event("cat")

interface EventListener<T: Event> {
    fun handle(event: T): Unit
}

Работает следующий тест:

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
}

Это хорошо работает, поскольку это автономная система, где полное игнорирование, которое мы показываем безопасности типов, на самом деле не подвергается опасным воздействиям и, следовательно, является "достаточно безопасным".Если я подумаю о лучшем способе, я обновлю публикацию.Одним из методов может быть создание новой Карты-класса-списков, с которой был связан контракт, который пытается связать тип значения с типом ключа и помогает устранить приведение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...