Putting a generic lambda into a map

2019-05-05 19:20发布

Ok so the code below is an event system that does the following:

  1. Assigns an integer id to a lambda expression
  2. Puts the lambda's id in an event's mutable set
  3. Maps the integer ID to the lambda expression
  4. Returns the id (can be used later to remove events from the lambda)

The code is as follows:

class EventHandler {
    companion object {
        val handlers = HashMap<KClass<out Event>, MutableSet<Int>>()
        val idMap = HashMap<Int, (Event) -> Unit>();

        /**
         * @param event     Class of the event you are registering
         * @param handler   What to do when the event is called
         */
        fun <T : Event> register(event: KClass<T>, handler: (T) -> Unit): Int {
            var id: Int = 0;
            while(idMap[id] != null) {
                id++;
            }
            var list = handlers.getOrPut(event, {mutableSetOf()});
            list.add(id);
            idMap[id] = handler;
            return id;
        }
    }
}

The intended use of this method would be something like this:

EventHandler.register(ChatEvent::class) { onChat ->
    println(onChat.message)
}

There is an error at the following line: idMap[id] = handler;

The error is because the handler is of type (T) -> Unit, although it needs to be (Event) -> Unit in order to add it to the idMap. Although I said that T should extend Event when I created it, so this shouldn't be a problem. Does anyone know why this happens of if there is a solution?

3条回答
Summer. ? 凉城
2楼-- · 2019-05-05 19:48

The problem comes from the fact that idMap takes a function that receives an Event - any kind of Event. Then you try to register a function that takes a specified subclass of Event, which, when you pull it back out, the compiler won't be able to tell what possible subclass it receives. Yes, you're storing the specific type in your other map, but the compiler can't use that.

I do not believe you can create the mapping system that you want to create. Not without a few more layers of indirection or abstraction...

查看更多
\"骚年 ilove
3楼-- · 2019-05-05 19:56

I figured out an implementation:

class Handlers<T: Event> {
    val backingList = ArrayList<(T) -> Unit>()

    fun add(handler: (T) -> Unit) {
        backingList.add(handler)
    }

    fun remove(handler: (T) -> Unit) {
         backingList.remove(handler)
    }

    fun handleEvent(event: T) {
        backingList.forEach { handle ->
            handle(event)
        }
    }
}

class HandlerMap {
     val handlerMap = HashMap<KClass<out Event>, Handlers<out Event>>()

    fun <T : Event> register(event: KClass<T>, handler: (T) -> Unit) {
        val list: Handlers<T>
        if(!handlerMap.containsKey(event)) {
            list = Handlers<T>()
            handlerMap.put(event, list)
        }
        else {
            list = handlerMap.get(event) as Handlers<T>
        }

        list.add(handler)        
    }

    fun <T: Event> getHandlers(event: KClass<T>): Handlers<T> {
        return handlerMap.get(event) as Handlers<T>
    }
}

It has to do some casting due to the Map being kind of open, but the system is closed, so the Handlers object is guaranteed to be of the exactly right type. Due to the implementation change, I was pretty sure you didn't care about the index/"id" anymore, but I tried an implementation with it, and it was fine; no real trouble to put it in if you want it.

查看更多
可以哭但决不认输i
4楼-- · 2019-05-05 20:02

The reason you get this error is explained well by @jacob-zimmerman in his answer: (T) -> Unit is not a subtype of (Event) -> Unit (on the contrary it is a supertype).

You could make an unchecked downcast to required function type:

idMap[id] = handler as (Event) -> Unit

But then before invoking such handler you must check that an event has type that the handler could accept, for example by querying handler from map based on the type of the event:

fun invoke(event: Event) {
    val kclass = event.javaClass.kotlin
    val eventHandlers = handlers[kclass]?.map { idMap[it]!! } ?: return
    eventHandlers.forEach { it.invoke(event) }
}
查看更多
登录 后发表回答